mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Combine controller stack with validated PGXP fixes and current master; regenerate both BIOS backends
This commit is contained in:
@@ -54,6 +54,7 @@ constrains this repository's own terms; the obligation is to carry the notice.
|
||||
| --- | --- | --- | --- |
|
||||
| [toml11](https://github.com/ToruNiina/toml11) by Toru Niina | MIT | recompiler **and** runtime | `recompiler/lib/toml11/LICENSE`, shipped as `runtime/licenses/toml11-NOTICES.txt` |
|
||||
| [stb_image](http://nothings.org/stb) by Sean Barrett | MIT **or** public domain (Unlicense), at your option | runtime | notice in `runtime/third_party/stb_image.h`, shipped as `runtime/licenses/stb_image-NOTICES.txt` |
|
||||
| [xxHash 0.8.3](https://github.com/Cyan4973/xxHash/tree/v0.8.3) by Yann Collet | BSD-2-Clause | runtime texture-pack interoperability | `runtime/third_party/xxhash.LICENSE`, shipped as `runtime/licenses/xxhash-NOTICES.txt` |
|
||||
| [rabbitizer](https://github.com/Decompollaborate/rabbitizer) by Decompollaborate | MIT | recompiler only | `recompiler/lib/rabbitizer/LICENSE` |
|
||||
| [ELFIO](https://github.com/serge1/ELFIO) by Serge Lamikhov-Center | MIT | recompiler only | `recompiler/lib/ELFIO/LICENSE.txt` |
|
||||
| [{fmt}](https://github.com/fmtlib/fmt) by Victor Zverovich | MIT | recompiler only | `recompiler/lib/fmt/LICENSE.rst` |
|
||||
@@ -65,6 +66,15 @@ runtime dependency lands. The recompiler-only entries are developer tooling
|
||||
and are not in the shipped package; if that ever changes, their notices have
|
||||
to ship too.
|
||||
|
||||
The unmodified xxHash header comes directly from upstream tag `v0.8.3`,
|
||||
`xxhash.h` (SHA-256
|
||||
`17973c0dc49d9854ca26caa191f0e12f7a424b68858d9a78de3860d959d85e4b`).
|
||||
Its BSD notice remains in the header and the separate notice files. The
|
||||
DuckStation-compatible filename parser, matching, PNG dumping and cache are
|
||||
independent psxrecomp implementations; no DuckStation implementation is
|
||||
included. Format observations and the upstream reference pin are recorded in
|
||||
[DUCKSTATION_TEXTURE_FORMAT.md](docs/DUCKSTATION_TEXTURE_FORMAT.md).
|
||||
|
||||
## TinyCC (TCC) — toolchain-free overlay compiler shipped to players
|
||||
|
||||
[TinyCC](https://bellard.org/tcc/) by Fabrice Bellard and contributors, licensed
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# DuckStation texture-pack interoperability
|
||||
|
||||
psxrecomp implements a bounded subset of modern DuckStation texture packs.
|
||||
The parser, matcher, cache and dumper are independently written here. The
|
||||
reference version is upstream commit
|
||||
`697599c47a646a6cfcc4d246018adf4904d55001`, observed on 2026-10-06.
|
||||
This pin matters for palette-range and alpha behavior.
|
||||
|
||||
## Provenance
|
||||
|
||||
At the reference pin, DuckStation's
|
||||
[license](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/LICENSE)
|
||||
is CC-BY-NC-ND-4.0; its texture-cache and shader files carry the same SPDX
|
||||
identifier. This project includes no DuckStation implementation, translated
|
||||
implementation, patch, or binary. Its own code retains the project's
|
||||
PolyForm Noncommercial terms. Reading format behavior does not provide a
|
||||
license to incorporate DuckStation's source. Pack artwork has its own rights;
|
||||
users supply it separately.
|
||||
|
||||
The hash implementation comes directly from
|
||||
[xxHash v0.8.3](https://github.com/Cyan4973/xxHash/tree/v0.8.3), under
|
||||
[BSD-2-Clause](https://github.com/Cyan4973/xxHash/blob/v0.8.3/LICENSE).
|
||||
The unmodified `runtime/third_party/xxhash.h` has SHA-256
|
||||
`17973c0dc49d9854ca26caa191f0e12f7a424b68858d9a78de3860d959d85e4b`.
|
||||
Its license ships as `runtime/licenses/xxhash-NOTICES.txt`.
|
||||
The [v0.8.0 release](https://github.com/Cyan4973/xxHash/releases/tag/v0.8.0)
|
||||
made XXH3 output stable across future versions. Thus the upstream v0.8.3
|
||||
implementation supplies the unseeded XXH3 behavior used by DuckStation's
|
||||
vendored v0.8.0 without importing any emulator code.
|
||||
|
||||
## Supported files
|
||||
|
||||
Choose a game/package folder containing `replacements/` and `dumps/`, or choose
|
||||
`replacements/` itself. Replacement PNGs can live in subdirectories. See the
|
||||
[player workflow](HD_TEXTURE_PACKS.md) for dumping, editing and reloading.
|
||||
DuckStation documents its serial-based folders and authoring process in its
|
||||
[texture replacement wiki](https://github.com/stenzek/duckstation/wiki/Texture-Replacement).
|
||||
|
||||
Paletted filename stems follow this grammar:
|
||||
|
||||
```text
|
||||
<kind>-<mode>-<sourceHash>-<paletteHash>-<sourceWords>x<sourceHeight>-<offsetX>-<offsetY>-<width>x<height>-P<min>-<max>
|
||||
kind = texupload | texpage
|
||||
mode = P4 | P8 | STP4 | STP8
|
||||
```
|
||||
|
||||
Direct stems omit the palette fields:
|
||||
|
||||
```text
|
||||
<kind>-<mode>-<sourceHash>-<sourceWords>x<sourceHeight>-<offsetX>-<offsetY>-<width>x<height>
|
||||
mode = C16 | STC16
|
||||
```
|
||||
|
||||
Append `.png`. Hash fields contain exactly 16 hexadecimal digits; either case
|
||||
is accepted. Dimensions, offsets and palette endpoints are decimal. Source
|
||||
width is in native VRAM words. Rectangle width and X offset are expanded
|
||||
texels: four per word for P4, two for P8, one for C16. Height and Y offset are
|
||||
rows. A page's source size is 64x256, 128x256 or 256x256 words respectively.
|
||||
These definitions follow the
|
||||
[upstream filename documentation](https://github.com/stenzek/duckstation/wiki/Texture-Replacement).
|
||||
|
||||
The loader validates nonzero sizes, native VRAM bounds, word-aligned horizontal
|
||||
rectangles, page bounds and palette endpoints. The replacement PNG's actual
|
||||
size controls its independent X/Y scale; integer and equal-axis scale factors
|
||||
are not required. The renderer applies the declared native rectangle's origin
|
||||
and extent when mapping UVs.
|
||||
|
||||
## Hash compatibility
|
||||
|
||||
Both texture and palette identities use unseeded XXH3-64 over packed native
|
||||
PSX words. Words are serialized low byte first. Rectangles concatenate each
|
||||
row's words without stride padding. `texupload` hashes the complete tracked
|
||||
post-write source rectangle; `texpage` hashes its declared subrectangle.
|
||||
Dimensions and palette data do not enter the texture hash.
|
||||
|
||||
A full CLUT hashes 16 P4 or 256 P8 words, including bit 15. A P8 CLUT extending
|
||||
beyond VRAM's right edge hashes only the remaining words, without wrapping.
|
||||
For reduced `Pmin-max` ranges, the reference implementation hashes the **first
|
||||
`max-min+1` palette words**, without advancing by `min`; a range whose declared
|
||||
maximum crosses the edge does not match. It does not revalidate used indices
|
||||
against the declared range during lookup. psxrecomp preserves this behavior
|
||||
for interoperability. Changing a palette entry outside that prefix can leave
|
||||
a replacement matched; use full ranges when authoring new packs here. These
|
||||
are observations of the pinned
|
||||
[texture-cache behavior](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/src/core/gpu_hw_texture_cache.cpp).
|
||||
|
||||
## Alpha conventions
|
||||
|
||||
The decoder preserves raw RGBA bytes. Ordinary P4/P8/C16 replacements treat
|
||||
alpha below 128 as cutout and alpha at least 128 as occupied, including black.
|
||||
ST names encode PSX transparency classification: alpha at most 242 marks
|
||||
STP, while alpha at least 243 marks an opaque texel. Exact RGBA zero is cutout;
|
||||
ST black with alpha at least 243 also becomes PSX transparent zero. Alpha zero
|
||||
with nonzero RGB in an ST image remains a semitransparent texel. Actual blending
|
||||
still uses the primitive's PSX transparency mode. These rules follow the pinned
|
||||
[replacement merge shader](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/src/core/gpu_hw_shadergen.cpp).
|
||||
|
||||
When both ordinary and ST records match, psxrecomp prefers the primitive's
|
||||
convention; if only the other convention exists it can still supply the image.
|
||||
Multiple matching records with the same preferred convention fall back to
|
||||
native rendering. This deterministic policy avoids dependence on directory
|
||||
enumeration or unordered-map order.
|
||||
|
||||
The dumper writes rounded 5-bit-to-8-bit native RGB, full CLUT ranges, alpha
|
||||
zero for native color zero, alpha 255 for occupied ordinary texels, and alpha
|
||||
128 for occupied STP texels in ST dumps. The pinned upstream dump produces
|
||||
143 in that last case because of an incidental mask expression; both values
|
||||
have the same replacement classification. Dumps here are compatible authoring
|
||||
inputs, not a promise of byte-identical upstream dump PNGs. Native conversion
|
||||
was checked against the pinned
|
||||
[GPU helpers](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/src/core/gpu_helpers.h).
|
||||
|
||||
## Current compatibility limits
|
||||
|
||||
The module accepts a single replacement covering the complete draw query.
|
||||
It supports ordinary unchanged uploads and nonwrapping page subrectangles.
|
||||
Every touched upload loses its identity after a partial overwrite; it does
|
||||
not reconstruct split or coalesced upload histories. A same-session pack reload
|
||||
can copy intact upload identities into the newly indexed pack. Loading a
|
||||
savestate clears them; page matching remains available, while upload matching
|
||||
needs subsequent guest uploads. The module's rectangle and query caches must
|
||||
be invalidated on every native VRAM write.
|
||||
|
||||
The following cases use native rendering or report an unsupported-file/option
|
||||
diagnostic:
|
||||
|
||||
- Wrapped UV intervals, horizontally wrapped pages and wrapped uploads.
|
||||
- Draws requiring composition from several replacement images.
|
||||
- `vram-write-` XXH3-128 images, JPEG/WebP payloads and legacy unnamed layouts.
|
||||
- Copy-to-upload conversion, upload coalescing and split-upload identities.
|
||||
- Other `config.yaml` behavior options, including replacement bilinear filtering.
|
||||
|
||||
`config.yaml` supports only a bounded `Aliases:` mapping with literal or quoted
|
||||
filename keys and relative PNG paths. A single-line literal/folded block value
|
||||
is accepted. Escaped strings, multiline values, anchors and general YAML
|
||||
features are outside the subset. Absolute paths and `..` traversal are
|
||||
rejected. A direct canonical replacement takes precedence over its alias.
|
||||
Unknown top-level options are reported by name and are not applied.
|
||||
|
||||
## Bounds and lifecycle
|
||||
|
||||
The loader hard-fails above 262144 candidate files or entries, scans at most
|
||||
1048576 directory entries and limits recursive nesting to 16 levels. Duplicate
|
||||
identities are marked ambiguous. Matching indexes upload hashes and page
|
||||
geometry, with 256 rectangle hashes and 256 positive/negative query results.
|
||||
Native page/CLUT writes invalidate affected cached queries. A query requiring
|
||||
over 4096 candidate page geometries or more than 4 MiB of new hashing reports
|
||||
an error and renders natively.
|
||||
|
||||
One low-priority background worker performs PNG decoding and dumping. The
|
||||
decode queue is bounded to 32, decoded metadata to 512 entries, and decoded
|
||||
pixels to a default 64 MiB budget. Encoded files are limited to 64 MiB and each
|
||||
image dimension to 8192 before decoding. Pixel leases survive eviction and
|
||||
pack destruction. Decoding failure leaves native rendering active.
|
||||
|
||||
Dumps use word-aligned draw rectangles, choose `texupload` when an intact
|
||||
tracked source contains the draw, and otherwise choose `texpage`. The queue
|
||||
holds at most eight snapshots. A bounded set deduplicates 8192 dump identities
|
||||
per session; reaching it reports a diagnostic until dump tracking is reset.
|
||||
PNG writing finishes on pack destruction. A temporary file is published
|
||||
atomically without replacing an existing final file. Write failures remove
|
||||
only the created temporary, print an error even on shutdown, and are returned
|
||||
by the next dump call. All snapshots decode native VRAM rather than replacement
|
||||
pixels.
|
||||
|
||||
## Independent validation
|
||||
|
||||
`runtime/tests/test_duckstation_texture_pack.cpp` contains fixed XXH3 vectors
|
||||
generated with the separate Python xxhash 3.6.0 native wheel. Input word `i`
|
||||
is `((i * 7919) ^ 0xa51c) & 0xffff`, serialized little endian. Examples:
|
||||
|
||||
| Word count | XXH3-64 |
|
||||
| --- | --- |
|
||||
| 0 | `2D06800538D394C2` |
|
||||
| 4 | `3631D2A04EC85311` |
|
||||
| 8 | `000CA76CB9B329DC` |
|
||||
| 16 | `9CBCDEA9305D7528` |
|
||||
| 256 | `0005D0B83A005F4E` |
|
||||
| 8192 | `C68A00DCCA652920` |
|
||||
|
||||
The test also checks literal filenames, strided native rectangles, crop
|
||||
coordinates, CLUT edges and reduced-prefix behavior, positive/negative cache
|
||||
invalidation, reload identity copying, duplicate ambiguity, alias diagnostics,
|
||||
raw alpha thresholds, Unicode folders, background decoding, native dump pixels,
|
||||
worker shutdown and preservation of existing edited files. Renderer fixtures
|
||||
exercise the alpha classification and native-VRAM authority separately.
|
||||
@@ -0,0 +1,66 @@
|
||||
# HD texture packs
|
||||
|
||||
The **HD Texture Packs** mod loads compatible DuckStation texture PNGs and can
|
||||
dump original textures encountered during gameplay. It is disabled by default.
|
||||
Use **OpenGL** to display replacements. Software and Vulkan can collect dumps
|
||||
but continue to display the original artwork. Tomba USA (`SCUS-94236`) is the
|
||||
first integrated game target; game projects can supply their own manifest for
|
||||
the same `psx.hd-textures` plugin.
|
||||
|
||||
Tomba provides an [optional drop-in example pack](https://github.com/mstan/TombaRecomp/tree/master/examples/hd-texture-pack)
|
||||
with five synthetic checkerboards and a README inside its `SCUS-94236` pack
|
||||
directory. It includes no original game artwork or dumps and remains opt-in.
|
||||
Copy its `mods` folder into the game's writable directory, then enable the mod
|
||||
and select OpenGL to see the example title textures.
|
||||
|
||||
## Install a pack
|
||||
|
||||
1. Open the launcher's **Mods** page and enable **HD Texture Packs**.
|
||||
2. Under **Texture pack folder**, click **Open folder**. On first use the
|
||||
launcher creates `mods/texture-packs/<game-id>` beside the executable, with
|
||||
empty `replacements` and `dumps` directories.
|
||||
3. Copy the pack's replacement PNGs into `replacements`, preserving their
|
||||
filenames. Subdirectories are supported. If the download already contains
|
||||
`replacements`, copy that directory's contents into this one.
|
||||
4. Leave **Load replacements** enabled and **Dump textures** disabled.
|
||||
5. Select **OpenGL** in Settings, then click **Play**.
|
||||
|
||||
**Change folder** selects another pack root. Empty folders are valid; their
|
||||
`replacements` and `dumps` directories are created when the pack is configured.
|
||||
You can also select a pack's `replacements` directory directly, with `dumps`
|
||||
beside it. Keep editable packs outside `mods/bundled`, which builds regenerate.
|
||||
|
||||
## Dump and edit textures
|
||||
|
||||
1. Enable **HD Texture Packs**, disable **Load replacements**, and enable
|
||||
**Dump textures**.
|
||||
2. Click **Play** and visit the scenes you want to capture. Original texture
|
||||
PNGs appear under `dumps` as the game uses them.
|
||||
3. Exit to the launcher. Copy selected PNGs into `replacements`, then edit or
|
||||
enlarge them while keeping the filenames, aspect ratio, and an integer
|
||||
size multiplier.
|
||||
4. Enable **Load replacements**, disable **Dump textures**, and click **Play**
|
||||
using OpenGL to check the artwork.
|
||||
|
||||
The switches apply on **Play**. Restart after adding or editing pack files so
|
||||
the runtime scans them again. Disabling the mod restores the original artwork.
|
||||
The plugin does not write replacement pixels into native VRAM or change the
|
||||
save namespace.
|
||||
|
||||
## Compatibility and diagnostics
|
||||
|
||||
Pack artwork is supplied separately. Matching depends on native game pixels
|
||||
and palettes; a different region or revision may not match. PNG is supported,
|
||||
with a maximum of 8192 pixels per side, 64 MiB encoded, and 64 MiB decoded RGBA.
|
||||
Unsupported identities, configuration features, or images fall back to the
|
||||
original artwork. After loading a savestate, upload-based replacements need
|
||||
fresh texture uploads from the game before they can match again; page-based
|
||||
matching remains available. See [DuckStation texture format notes](DUCKSTATION_TEXTURE_FORMAT.md)
|
||||
for the exact supported filenames, hashing, alpha behavior, and limitations.
|
||||
|
||||
Development builds expose the [TCP `hd_textures` command](TCP_COMMANDS.md).
|
||||
It reports the selected root, switches, backend support, pack size, and matched,
|
||||
ready, and applied draw counts. Optional integer fields `replacements: 0|1`,
|
||||
`dump: 0|1`, and `reload: 1` control an already configured pack during a test
|
||||
session. Use `present_shot` to capture the composed OpenGL window; native VRAM
|
||||
screenshots retain the original artwork.
|
||||
@@ -55,6 +55,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `screenshot_wide_hires` | ✓ | | `path`, `base_x` | The displayed band of the native-wide surface at internal resolution (`wide_w×S` by `height×S`). `present_shot` is capped at the window; this checks a widescreen + internal-resolution combination at full size |
|
||||
| `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) |
|
||||
| `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates |
|
||||
| `hd_textures` | ✓ | | optional `replacements`, `dump` (0 or 1), `reload` (1) | HD pack root, switches, backend support, replacement count, matched/ready/applied draw counts, and queued dump count. Optional controls update the configured host pack on the emulation thread; no configured root is an error. Reload retains unchanged upload identities. OpenGL displays replacements; software/Vulkan retain original artwork. Pair with `present_shot` for visual evidence and native VRAM probes for architectural data |
|
||||
| `gl_interp` | ✓ | | — | OpenGL frame-rate presenter ([FRAME_RATE.md](FRAME_RATE.md)): enabled/suspended, host and target Hz, swaps, `source` (`vblank`/`flip`), `flip_period`, `captures` (new source frames) and `duplicates` (VBlanks that re-presented the same frame) |
|
||||
| `render_pass_stats` | ✓ | | — | Render passes ([RENDER_PASSES.md](RENDER_PASSES.md)): plans, phases wanted/planned (shedding), passes, rollbacks (`nesting_repairs`: watchdog aborts whose skipped frame exits the restore undid), dropped device stores by class, `verify_mismatch` under `PSX_RENDER_PASS_VERIFY=1`, host-time split per pass, smoothed pass cost (`cost_us`; `cost_rewarms`: estimates no pass had run on for a while, measured again), presents made from pass images (`late_presents`: held past the frame's planned end because the next flip was late; `expired`: frames whose images stopped showing after several frame lengths without a flip), pass image textures allocated (`image_textures`, `image_bytes`), `status` (`psx_mod_render_pass_status`: 0 ready, 1 no presenter, 2 backend, 3 disabled, 4 session, 5 fast-forward, 6 busy), `backups_reused` (passes that reused the previous pass's VRAM backup), `spans` / `span_failures` and the last failure `span_fail` (`psx_mod_run_guest_span`: reason, exit PC, `$ra`, PC after a call that did not return) |
|
||||
| `window_size` | ✓ | | `w`, `h` | Resize the game window as a player would (Fit / adaptive widescreen follow it): window-shaped checks in `--hidden-window` runs. Pair with `present_shot` |
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# DuckStation texture format and xxHash provenance
|
||||
|
||||
The independent format integration is described in
|
||||
[DuckStation texture format notes](../../DUCKSTATION_TEXTURE_FORMAT.md).
|
||||
Reference behavior was observed at DuckStation commit
|
||||
[697599c47a646a6cfcc4d246018adf4904d55001](https://github.com/stenzek/duckstation/tree/697599c47a646a6cfcc4d246018adf4904d55001),
|
||||
whose [license](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/LICENSE)
|
||||
is CC-BY-NC-ND-4.0. DuckStation is authored by Stenzek and its contributors;
|
||||
its authorship is credited by the pinned reference links. No DuckStation
|
||||
implementation, translated implementation, patch, or binary is included,
|
||||
and no DuckStation co-author trailer is claimed for this independent work.
|
||||
|
||||
Implemented behavior: bounded modern texture filenames and palette ranges,
|
||||
XXH3 identities over native pixel sources and palettes, supported alpha
|
||||
classification, replacement loading, and original-texture dumping. Related
|
||||
coalescing, copy/split identities, multi-image composition, wrapped footprints,
|
||||
JPEG/WebP decoding, and DuckStation rendering/cache implementation are excluded.
|
||||
The format guide records the exact compatibility limits.
|
||||
|
||||
The only vendored implementation is unmodified
|
||||
[xxHash v0.8.3](https://github.com/Cyan4973/xxHash/tree/v0.8.3), authored by Yann
|
||||
Collet and contributors under
|
||||
[BSD-2-Clause](https://github.com/Cyan4973/xxHash/blob/v0.8.3/LICENSE).
|
||||
`runtime/third_party/xxhash.h` retains its original copyright and license;
|
||||
its SHA-256 is
|
||||
`17973c0dc49d9854ca26caa191f0e12f7a424b68858d9a78de3860d959d85e4b`.
|
||||
Player packages carry `runtime/licenses/xxhash-NOTICES.txt` as a runtime
|
||||
notice. Vendoring preserves authorship in the unmodified source and notices,
|
||||
rather than attributing that source to this change's author.
|
||||
|
||||
Validation on current framework master `92c0f3cd`: registered DuckStation
|
||||
module and OpenGL renderer tests pass, alongside mod package, builtin
|
||||
manifest, runtime, and resident tests. Independent xxHash wheel vectors,
|
||||
literal filenames, PNG pixels and file preservation, cache/reload behavior,
|
||||
alpha handling, upload aborts, and native VRAM invariance are covered.
|
||||
Tomba USA title and Village gameplay showed synthetic replacements, with
|
||||
live off/reload/on and stable native texture atlas comparisons. Full scene
|
||||
coverage and third-party artwork packs were not claimed; the first consumer
|
||||
needs a framework pin update and runtime rebuild, without a game C regen
|
||||
for the HD texture feature itself.
|
||||
@@ -1 +1 @@
|
||||
c93ca7b2e8c1c1cdd94e3d28b3a4d77fe84417974776af7cf69b776c123e8d83
|
||||
b55f31a1f8d7abe4b04c0322dc961731687343f4ea14d3e722acdc0979f5e183
|
||||
|
||||
@@ -89042,6 +89042,7 @@ label_BFC057F0:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 8u;
|
||||
#endif
|
||||
PGXP_ALU(0x40086000u, cpu->gpr[8], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0xBFC057F0u);
|
||||
#endif
|
||||
@@ -94486,6 +94487,7 @@ label_BFC05CBC:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 17u;
|
||||
#endif
|
||||
PGXP_ALU(0x40114000u, cpu->gpr[17], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0xBFC05CD0u);
|
||||
#endif
|
||||
@@ -100322,6 +100324,7 @@ label_BFC06260:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 2u;
|
||||
#endif
|
||||
PGXP_ALU(0x40027800u, cpu->gpr[2], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0xBFC06260u);
|
||||
#endif
|
||||
@@ -111276,6 +111279,7 @@ label_BFC06C7C:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 2u;
|
||||
#endif
|
||||
PGXP_ALU(0x40026000u, cpu->gpr[2], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0xBFC06CA8u);
|
||||
#endif
|
||||
@@ -238712,6 +238716,7 @@ label_BFC20770:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 2u;
|
||||
#endif
|
||||
PGXP_ALU(0x40026800u, cpu->gpr[2], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x0000279Cu);
|
||||
#endif
|
||||
@@ -238726,6 +238731,7 @@ label_BFC20770:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 3u;
|
||||
#endif
|
||||
PGXP_ALU(0x40037000u, cpu->gpr[3], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x000027A0u);
|
||||
#endif
|
||||
@@ -239850,6 +239856,7 @@ label_BFC20800:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 4u;
|
||||
#endif
|
||||
PGXP_ALU(0x40046000u, cpu->gpr[4], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x0000286Cu);
|
||||
#endif
|
||||
@@ -239881,6 +239888,7 @@ label_BFC20800:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 5u;
|
||||
#endif
|
||||
PGXP_ALU(0x40056800u, cpu->gpr[5], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x00002878u);
|
||||
#endif
|
||||
|
||||
@@ -1 +1 @@
|
||||
a6c239ec2d16078269fda42c60a8abd4aaad690d3ec2d5c5aa50f1dd64529e5b
|
||||
eaafcbd6af676339d2b3a8c29b7394b8729325ab0ff6c117261b4207d2619735
|
||||
|
||||
@@ -24059,6 +24059,7 @@ label_BFC01920:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 8u;
|
||||
#endif
|
||||
PGXP_ALU(0x40086000u, cpu->gpr[8], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0xBFC01920u);
|
||||
#endif
|
||||
@@ -55366,6 +55367,7 @@ label_BFC03968:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 2u;
|
||||
#endif
|
||||
PGXP_ALU(0x40026000u, cpu->gpr[2], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0xBFC03968u);
|
||||
#endif
|
||||
@@ -214498,6 +214500,7 @@ label_BFC10120:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 2u;
|
||||
#endif
|
||||
PGXP_ALU(0x40026800u, cpu->gpr[2], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x00000620u);
|
||||
#endif
|
||||
@@ -214557,6 +214560,7 @@ label_BFC10130:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 2u;
|
||||
#endif
|
||||
PGXP_ALU(0x40026000u, cpu->gpr[2], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x00000630u);
|
||||
#endif
|
||||
@@ -215909,6 +215913,7 @@ label_BFC10800:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 4u;
|
||||
#endif
|
||||
PGXP_ALU(0x40046000u, cpu->gpr[4], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x00000D40u);
|
||||
#endif
|
||||
@@ -215937,6 +215942,7 @@ label_BFC10800:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 5u;
|
||||
#endif
|
||||
PGXP_ALU(0x40056800u, cpu->gpr[5], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x00000D4Cu);
|
||||
#endif
|
||||
@@ -216890,6 +216896,7 @@ label_BFC109A0:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 2u;
|
||||
#endif
|
||||
PGXP_ALU(0x40026800u, cpu->gpr[2], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x00000EA0u);
|
||||
#endif
|
||||
@@ -216901,6 +216908,7 @@ label_BFC109A0:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 3u;
|
||||
#endif
|
||||
PGXP_ALU(0x40037000u, cpu->gpr[3], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x00000EA4u);
|
||||
#endif
|
||||
@@ -661096,6 +661104,7 @@ label_BFC336F0:
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
cpu->ld_absorb = 0u; cpu->ld_which_t = 2u;
|
||||
#endif
|
||||
PGXP_ALU(0x40026000u, cpu->gpr[2], 0u, 0u);
|
||||
#ifdef PSX_COSIM
|
||||
cosim_instr(0x8004B6F4u);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
format_version = 5
|
||||
id = "psx.enhancement.hd-textures"
|
||||
version = "1.0.0"
|
||||
name = "HD Texture Packs"
|
||||
author = "psxrecomp"
|
||||
description = "Load compatible DuckStation PNG texture packs with OpenGL or dump original textures during gameplay. No artwork is bundled."
|
||||
resolver = "declarative"
|
||||
save_compatibility = "shared"
|
||||
|
||||
[[target]]
|
||||
game_id = "*"
|
||||
|
||||
[[feature]]
|
||||
id = "hd-textures"
|
||||
name = "HD Texture Packs"
|
||||
description = "Disabled by default. Open the pack folder to install PNG artwork in replacements, or enable Dump textures to collect originals in dumps. Options apply on Play. OpenGL displays replacements; other backends retain original artwork."
|
||||
group = "Visual"
|
||||
default_enabled = false
|
||||
|
||||
[[resource]]
|
||||
feature = "hd-textures"
|
||||
id = "pack"
|
||||
label = "Texture pack folder"
|
||||
description = "The launcher creates mods/texture-packs/<game-id> beside the executable on first use. Open folder to add replacement PNGs or read captured originals; Change folder selects another compatible pack or empty directory."
|
||||
format = "directory"
|
||||
required = true
|
||||
|
||||
[[option]]
|
||||
feature = "hd-textures"
|
||||
id = "replacements"
|
||||
label = "Load replacements"
|
||||
description = "Display matching PNG artwork from replacements in OpenGL. Unsupported or missing replacements retain the original artwork."
|
||||
type = "boolean"
|
||||
default = "true"
|
||||
|
||||
[[option]]
|
||||
feature = "hd-textures"
|
||||
id = "dump"
|
||||
label = "Dump textures"
|
||||
description = "Write original texture PNGs under dumps as they are encountered while playing. Disable after collecting the areas you need."
|
||||
type = "boolean"
|
||||
default = "false"
|
||||
|
||||
[[plugin]]
|
||||
feature = "hd-textures"
|
||||
id = "psx.hd-textures"
|
||||
@@ -102,6 +102,7 @@ int gr_render_display_hires(uint32_t *out_pixels, int out_pitch,
|
||||
int disp_x, int disp_y, int disp_w, int disp_h);
|
||||
|
||||
/* VRAM transfers */
|
||||
void gr_vram_upload_begin(int x, int y, int w, int h);
|
||||
void gr_vram_write(int x, int y, uint16_t pixel);
|
||||
uint16_t gr_vram_read(int x, int y);
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data);
|
||||
|
||||
@@ -270,6 +270,9 @@ void psx_mod_set_world_scene_predicate(PSXModWorldScenePredicate predicate);
|
||||
*/
|
||||
int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
const char* option_id, char* out, uint32_t out_size);
|
||||
/* The committed option for the trusted callback's owning package/feature. */
|
||||
int psx_mod_current_option_value(const char* option_id,
|
||||
char* out, uint32_t out_size);
|
||||
/*
|
||||
* Read the committed owner-selected path for a resource declared by the
|
||||
* package feature whose trusted plugin is currently running. Returns 0 when
|
||||
@@ -278,6 +281,12 @@ int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
*/
|
||||
int psx_mod_current_resource_path(const char* resource_id,
|
||||
char* out, uint32_t out_size);
|
||||
/* Configure an HD pack from the callback's selected directory resource.
|
||||
* Activation/emulation-thread only. Replacements require OpenGL. */
|
||||
int psx_mod_set_hd_texture_pack(const char* resource_id,
|
||||
int replacements_enabled, int dump_enabled);
|
||||
int psx_mod_set_hd_texture_dump(int enabled);
|
||||
int psx_mod_reload_hd_texture_pack(void);
|
||||
/* Read-only canonical media verified by the engine for this plugin's owning
|
||||
* package/feature. The pointer lives until the committed plan is replaced or
|
||||
* cleared. Available only during that plugin's callbacks; returns 0 for an
|
||||
|
||||
@@ -27,7 +27,9 @@ inline int16_t get_imm16(uint32_t i) { return (int16_t)(i & 0xFFFF); }
|
||||
* Every GPR-writing ALU op is hooked. Bitwise ops (AND/OR/XOR/NOR, ANDI/ORI/
|
||||
* XORI) carry a vertex half whose 11-bit GPU field survives them - engines
|
||||
* pack clip flags above the field after projecting - and SLT-family results
|
||||
* are reset, so no destination keeps a stale shadow. The configured
|
||||
* are reset, so no destination keeps a stale shadow. Coprocessor reads into
|
||||
* a GPR (MFC0, MFC2, CFC2) are hooked for the same reason: the interpreters
|
||||
* hook them, and an equal word is not proof of an equal vertex. The configured
|
||||
* widescreen special sites return early above translate_instruction's main
|
||||
* dispatch and are unhooked (they are cull compares, not vertex moves;
|
||||
* validation covers). */
|
||||
@@ -123,6 +125,16 @@ void append_pgxp_hooks(uint32_t instr, std::string& code) {
|
||||
code = fmt::format("{}\n PGXP_ALU(0x{:08X}u, {}, 0u, 0u);",
|
||||
code, instr, reg_name(rt));
|
||||
return;
|
||||
case 0x10: { /* COP0 register reads */
|
||||
/* MFC0 writes a GPR like any ALU op: reset its shadow so a status or
|
||||
* EPC word can never inherit an old vertex's precision. (CFC0 is not
|
||||
* translated by either emitter.) */
|
||||
const uint32_t cop_op = (instr >> 21) & 0x1F;
|
||||
if (cop_op == 0x00 && rt != 0)
|
||||
code = fmt::format("{}\n PGXP_ALU(0x{:08X}u, {}, 0u, 0u);",
|
||||
code, instr, reg_name(rt));
|
||||
return;
|
||||
}
|
||||
case 0x12: { /* COP2 register transfers */
|
||||
/* Control transfers too: engines pack tracked vertex halves into a
|
||||
* matrix row with CTC2 and use MVMVA as a weighted-sum multiplier
|
||||
|
||||
@@ -93,6 +93,33 @@ int main() {
|
||||
"a CFC2 to $zero writes no register and is not hooked");
|
||||
}
|
||||
|
||||
/* Coprocessor reads into a GPR replace its value like any ALU op. The
|
||||
* interpreters hook them; the compiled code must too, or the register
|
||||
* keeps the shadow of the vertex it held before. MFC0's emission ends on
|
||||
* the block-cycles #endif, so its hook also has to land on a new line. */
|
||||
{
|
||||
const uint32_t mfc0 = (0x10u << 26) | (0x00u << 21) | (3u << 16) | (12u << 11);
|
||||
std::string code =
|
||||
"cpu->gpr[3] = cpu->cop0[12];"
|
||||
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n cpu->ld_absorb = 0u; cpu->ld_which_t = 3u;\n#endif";
|
||||
PSXRecomp::append_pgxp_hooks(mfc0, code);
|
||||
CHECK(code.find("\n PGXP_ALU(0x40036000u, cpu->gpr[3], 0u, 0u);") != std::string::npos,
|
||||
"MFC0 resets the destination shadow on its own line");
|
||||
|
||||
const uint32_t cfc2 = (0x12u << 26) | (0x02u << 21) | (3u << 16) | (31u << 11);
|
||||
code = "cpu->gpr[3] = gte_read_ctrl(cpu, 31);";
|
||||
PSXRecomp::append_pgxp_hooks(cfc2, code);
|
||||
CHECK(code.find("PGXP_COP2(0x4843F800u, cpu->gpr[3], 0u);") != std::string::npos,
|
||||
"CFC2 resets the destination shadow");
|
||||
|
||||
code = "(void)cpu->cop0[12];";
|
||||
PSXRecomp::append_pgxp_hooks(mfc0 & ~(31u << 16), code);
|
||||
CHECK(code.find("PGXP_") == std::string::npos, "MFC0 to $zero is not hooked");
|
||||
code = "(void)gte_read_ctrl(cpu, 31);";
|
||||
PSXRecomp::append_pgxp_hooks(cfc2 & ~(31u << 16), code);
|
||||
CHECK(code.find("PGXP_") == std::string::npos, "CFC2 to $zero is not hooked");
|
||||
}
|
||||
|
||||
std::printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -42,12 +42,14 @@ def make_psxexe():
|
||||
for offset in (0x100, 0x200, 0x1100):
|
||||
for i, word in enumerate((0x27BDFFF0, 0x03E00008, 0x27BD0010)):
|
||||
put32(text, offset + i * 4, word)
|
||||
# Valid primary opcodes with unsupported SPECIAL subfields followed by a
|
||||
# return: discovery admits the entry, then production emission correctly
|
||||
# Valid primary opcodes with unsupported COP0 memory transfers followed by
|
||||
# a return: discovery admits the entry, then production emission correctly
|
||||
# demotes the >=50%-untranslatable body to a fail-closed data stub.
|
||||
# Undefined SPECIAL words are now intentionally emitted as compatible
|
||||
# no-ops, so they no longer exercise the untranslatable-data guard.
|
||||
for i, word in enumerate((
|
||||
0x27BDFFF0,
|
||||
0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000001,
|
||||
0xC0000000, 0xC0000000, 0xC0000000, 0xC0000000, 0xC0000000,
|
||||
jal(LOAD + 0x100), 0,
|
||||
0x03E00008, 0x27BD0010)):
|
||||
put32(text, 0x300 + i * 4, word)
|
||||
|
||||
@@ -203,6 +203,15 @@ if(BUILD_TESTING)
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-filter-results")
|
||||
set_tests_properties(gl_texture_filter_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 180)
|
||||
add_test(NAME gl_hd_texture_pack_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
|
||||
--hd-pack --fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_hd_texture_pack.c"
|
||||
--cc "${CMAKE_C_COMPILER}" --cxx "${CMAKE_CXX_COMPILER}"
|
||||
--sdl-include "$<TARGET_PROPERTY:SDL3::Headers,INTERFACE_INCLUDE_DIRECTORIES>"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-hd-texture-results")
|
||||
set_tests_properties(gl_hd_texture_pack_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 180)
|
||||
endif()
|
||||
add_executable(launcher_device_roundtrip_test
|
||||
tests/test_launcher_device_roundtrip.cpp)
|
||||
@@ -221,6 +230,14 @@ if(BUILD_TESTING)
|
||||
add_test(NAME launcher_pad_mode_resolve_test
|
||||
COMMAND launcher_pad_mode_resolve_test)
|
||||
|
||||
# Presentation-only mod input must not be sourced from disconnected,
|
||||
# peer-synchronized, or rollback-replayed pad state.
|
||||
add_executable(mod_local_input_scope_test
|
||||
tests/test_mod_local_input_scope.c)
|
||||
target_include_directories(mod_local_input_scope_test PRIVATE src)
|
||||
add_test(NAME mod_local_input_scope_test
|
||||
COMMAND mod_local_input_scope_test)
|
||||
|
||||
# COP0.CAUSE IP2 must mirror the INTC line combinationally, never latch.
|
||||
add_executable(cause_ip2_combinational_test
|
||||
tests/test_cause_ip2_combinational.c)
|
||||
@@ -324,6 +341,29 @@ if(BUILD_TESTING)
|
||||
$<IF:$<PLATFORM_ID:Darwin>,-Wl$<COMMA>-dead_strip,-Wl$<COMMA>--gc-sections>)
|
||||
target_link_libraries(isc_store_test PRIVATE ZLIB::ZLIB)
|
||||
add_test(NAME isc_store_test COMMAND isc_store_test)
|
||||
|
||||
# DMA and host stores drop the PGXP shadow of the words they touch
|
||||
# (an identical word is not proof of the same projection): the REAL
|
||||
# memory.c store paths against the REAL pgxp.cpp engine. Same link
|
||||
# doubles as isc_store_test, minus the PGXP one.
|
||||
add_executable(pgxp_untracked_store_test
|
||||
tests/test_pgxp_untracked_store.c
|
||||
tests/psx_ram_runtime_map_stubs.c
|
||||
src/memory.c
|
||||
src/dma.c
|
||||
src/gpu_timeline.c
|
||||
src/boot_state.c
|
||||
src/pgxp.cpp
|
||||
src/psx_ram_geometry.c)
|
||||
target_include_directories(pgxp_untracked_store_test PRIVATE include src)
|
||||
target_compile_definitions(pgxp_untracked_store_test PRIVATE
|
||||
PSX_STUBS_REAL_PGXP=1)
|
||||
target_compile_options(pgxp_untracked_store_test PRIVATE
|
||||
-O2 -flto -ffunction-sections -fdata-sections -Wno-lto-type-mismatch)
|
||||
target_link_options(pgxp_untracked_store_test PRIVATE -flto -Wno-lto-type-mismatch
|
||||
$<IF:$<PLATFORM_ID:Darwin>,-Wl$<COMMA>-dead_strip,-Wl$<COMMA>--gc-sections>)
|
||||
target_link_libraries(pgxp_untracked_store_test PRIVATE ZLIB::ZLIB)
|
||||
add_test(NAME pgxp_untracked_store_test COMMAND pgxp_untracked_store_test)
|
||||
endif()
|
||||
|
||||
# Loadable BIOS backends (psx_bios_module.h): the host loader against
|
||||
@@ -396,6 +436,12 @@ if(BUILD_TESTING)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(hd_texture_pack_test PRIVATE Threads::Threads)
|
||||
add_test(NAME hd_texture_pack_test COMMAND hd_texture_pack_test)
|
||||
add_executable(duckstation_texture_pack_test
|
||||
tests/test_duckstation_texture_pack.cpp
|
||||
src/duckstation_texture_pack.cpp)
|
||||
target_include_directories(duckstation_texture_pack_test PRIVATE include)
|
||||
target_link_libraries(duckstation_texture_pack_test PRIVATE Threads::Threads)
|
||||
add_test(NAME duckstation_texture_pack_test COMMAND duckstation_texture_pack_test)
|
||||
|
||||
# Standalone serial-pad protocol tests use explicit SIO/device stubs; they
|
||||
# cover both DualShock rumble and JogCon wire/state behavior.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#ifndef PSXRECOMP_DUCKSTATION_TEXTURE_PACK_H
|
||||
#define PSXRECOMP_DUCKSTATION_TEXTURE_PACK_H
|
||||
|
||||
#include "hd_texture_pack.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DuckTexturePack DuckTexturePack;
|
||||
|
||||
enum DuckTextureKind { DUCK_TEXTURE_UPLOAD = 0, DUCK_TEXTURE_PAGE = 1 };
|
||||
|
||||
/* Independent representation of the modern filename wire format. Source
|
||||
* width is in VRAM words; offsets and rectangle sizes are expanded texels.
|
||||
* Hashes are unseeded XXH3-64 over explicitly little-endian native words. */
|
||||
typedef struct DuckTextureKey {
|
||||
uint64_t source_hash;
|
||||
uint64_t palette_hash;
|
||||
uint16_t source_width_words;
|
||||
uint16_t source_height;
|
||||
uint16_t offset_x;
|
||||
uint16_t offset_y;
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t kind;
|
||||
uint8_t depth; /* enum HdTextureDepth */
|
||||
uint8_t semitransparent; /* STP4 / STP8 / STC16 alpha convention */
|
||||
uint8_t palette_min;
|
||||
uint8_t palette_max;
|
||||
} DuckTextureKey;
|
||||
|
||||
typedef struct DuckTexturePackInfo {
|
||||
const char* root;
|
||||
const char* diagnostic; /* unsupported config/formats and malformed names */
|
||||
size_t replacement_count;
|
||||
size_t ambiguous_count;
|
||||
size_t ignored_count;
|
||||
} DuckTexturePackInfo;
|
||||
|
||||
typedef struct DuckTextureMatch {
|
||||
DuckTextureKey key;
|
||||
uint64_t entry_id;
|
||||
const char* replacement_path;
|
||||
/* Origin of replacement rectangle relative to current texture page, in
|
||||
* texels. May be negative for uploads spanning more than one page. */
|
||||
int32_t origin_u;
|
||||
int32_t origin_v;
|
||||
uint16_t source_width;
|
||||
uint16_t source_height;
|
||||
} DuckTextureMatch;
|
||||
|
||||
typedef struct DuckTexturePixels {
|
||||
const uint8_t* rgba;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t stride;
|
||||
void* lease;
|
||||
} DuckTexturePixels;
|
||||
|
||||
/* Accepts a game/package folder containing replacements/ and dumps/, or the
|
||||
* replacements directory itself. Missing folders are valid empty packs, so
|
||||
* a new pack can start by dumping. Recursive PNG scan; no game assets bundled.
|
||||
* Non-PNG payloads, unsupported options and invalid names are diagnosed. */
|
||||
int duck_texture_pack_create(const char* root, DuckTexturePack** out_pack,
|
||||
char* error, size_t error_capacity);
|
||||
void duck_texture_pack_destroy(DuckTexturePack* pack);
|
||||
void duck_texture_pack_get_info(const DuckTexturePack* pack,
|
||||
DuckTexturePackInfo* out_info);
|
||||
|
||||
int duck_texture_parse_name(const char* filename, DuckTextureKey* out_key);
|
||||
int duck_texture_format_name(const DuckTextureKey* key, char* output,
|
||||
size_t capacity); /* stem only, without .png */
|
||||
uint64_t duck_texture_hash_words_le(const uint16_t* words, size_t count);
|
||||
/* Native word rectangle must not wrap. Zero on invalid input (also a valid
|
||||
* hash value); use valid geometry rather than zero as a success indication. */
|
||||
uint64_t duck_texture_hash_rect(const uint16_t* vram, size_t count,
|
||||
uint16_t x, uint16_t y,
|
||||
uint16_t width_words, uint16_t height);
|
||||
|
||||
/* Track post-write native VRAM, after PSX set/check-mask semantics. Wrapped
|
||||
* uploads are invalidated but deliberately not tracked. Copies, coalescing,
|
||||
* and split-upload identities are outside this initial compatibility subset.
|
||||
* Any touched upload is dropped, so partial overwrites fail conservatively. */
|
||||
int duck_texture_pack_track_upload(DuckTexturePack* pack,
|
||||
uint16_t x, uint16_t y,
|
||||
uint16_t width_words, uint16_t height,
|
||||
const uint16_t* vram, size_t count);
|
||||
void duck_texture_pack_invalidate(DuckTexturePack* pack,
|
||||
uint16_t x, uint16_t y,
|
||||
uint16_t width_words, uint16_t height);
|
||||
void duck_texture_pack_reset_tracking(DuckTexturePack* pack);
|
||||
/* Same-session reload only: copy immutable upload identities while the caller
|
||||
* keeps native VRAM unchanged. Lookup, image and dump caches stay independent.
|
||||
* This is not a savestate restoration API. Strong replacement on success. */
|
||||
int duck_texture_pack_copy_tracking(DuckTexturePack* destination,
|
||||
const DuckTexturePack* source);
|
||||
|
||||
/* Only one replacement covering the entire inclusive query is accepted.
|
||||
* UV wrapping, edge-wrapped pages and multi-image composition fall back.
|
||||
* Uses HD_TEXTURE_LOOKUP_* statuses. Query geometry is shared with Beetle. */
|
||||
int duck_texture_pack_match(DuckTexturePack* pack,
|
||||
const HdTextureDrawQuery* query,
|
||||
DuckTextureMatch* out_match);
|
||||
/* Prefer ST names on semitransparent primitives and ordinary names otherwise;
|
||||
* when only the other convention exists it remains usable. The filename's
|
||||
* convention always determines how the renderer interprets its alpha. */
|
||||
int duck_texture_pack_match_draw(DuckTexturePack* pack,
|
||||
const HdTextureDrawQuery* query,
|
||||
int semitransparent, DuckTextureMatch* out_match);
|
||||
|
||||
/* One bounded background worker; no PNG decoding in the render thread.
|
||||
* Pixels retain raw DuckStation alpha bytes. The renderer interprets the
|
||||
* key.semitransparent flag; this is not conventional opacity for ST names.
|
||||
* Release the lease even after pack destruction. */
|
||||
void duck_texture_pack_set_decode_budget(DuckTexturePack* pack, size_t bytes);
|
||||
int duck_texture_pack_request_decode(DuckTexturePack* pack, uint64_t entry_id);
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* pack, uint64_t entry_id,
|
||||
DuckTexturePixels* out_pixels);
|
||||
void duck_texture_pixels_release(DuckTexturePixels* pixels);
|
||||
|
||||
/* Dumps the word-aligned draw rectangle. Uses texupload when exactly one
|
||||
* unchanged tracked upload covers it; otherwise texpage. Full palette range
|
||||
* is emitted; transparent native zero stays zero, native STP uses alpha 128
|
||||
* for ST names. Existing files are never overwritten. Writes native decoded
|
||||
* pixels only, never already-replaced pixels. Returns FOUND for a new dump,
|
||||
* NONE for an existing/skipped dump, ERROR on invalid input or write failure.
|
||||
* FOUND means queued; bounded background writes finish on pack destruction.
|
||||
* Any asynchronous write failure is reported by the next dump call. */
|
||||
int duck_texture_pack_dump_draw(DuckTexturePack* pack,
|
||||
const HdTextureDrawQuery* query,
|
||||
int semitransparent,
|
||||
char* error, size_t error_capacity);
|
||||
void duck_texture_pack_reset_dump(DuckTexturePack* pack);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -25,6 +25,10 @@ int gl_renderer_select_texture_bank(uint16_t id);
|
||||
/* Retain texture indices while sampling current guest CLUTs (fades/animation). */
|
||||
int gl_renderer_select_texture_bank_live_clut(uint16_t id);
|
||||
int gl_renderer_texture_banks_supported(void);
|
||||
/* HD texture presentation uses an independent native SW VRAM authority.
|
||||
* Both entry points drain pending draws before changing or deleting textures. */
|
||||
void gl_renderer_set_hd_texture_mode(int enabled);
|
||||
void gl_renderer_clear_hd_texture_cache(void);
|
||||
|
||||
/* Set the GL swap interval / vsync mode (1=vsync, 0=immediate, -1=adaptive).
|
||||
* Safe before or after context creation; applies live when a context exists. */
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef PSXRECOMP_GPU_HD_TEXTURES_H
|
||||
#define PSXRECOMP_GPU_HD_TEXTURES_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Emulation-thread configuration. Roots are external user-owned directories;
|
||||
* a successful replacement session selects native CPU VRAM authority on GL.
|
||||
* Reconfiguration retains the previous session if opening the new root fails.
|
||||
* Empty root with both options off clears the current session. */
|
||||
int gpu_hd_textures_configure(const char* root, int replacements_enabled,
|
||||
int dump_enabled, char* error, size_t capacity);
|
||||
void gpu_hd_textures_shutdown(void);
|
||||
int gpu_hd_textures_reload(char* error, size_t capacity);
|
||||
void gpu_hd_textures_set_dump_enabled(int enabled);
|
||||
int gpu_hd_textures_replacements_enabled(void);
|
||||
int gpu_hd_textures_dump_enabled(void);
|
||||
int gpu_hd_textures_active(void);
|
||||
typedef struct GpuHdTextureDiag {
|
||||
const char* root;
|
||||
int active, replacements, dump, format;
|
||||
uint64_t draw_queries, matched_draws, ready_draws, applied_draws, dumped_textures;
|
||||
size_t replacement_count;
|
||||
} GpuHdTextureDiag;
|
||||
void gpu_hd_textures_get_diag(GpuHdTextureDiag* out);
|
||||
void gpu_hd_textures_note_applied(void);
|
||||
/* Non-GL backends can observe native draws for dumping; replacements remain
|
||||
* an OpenGL presentation feature. */
|
||||
void gpu_hd_textures_observe_draw(uint16_t texpage, uint16_t clut_x,
|
||||
uint16_t clut_y, const int limits[4],
|
||||
uint32_t texture_window, int semitransparent);
|
||||
|
||||
/* Native VRAM and residency hooks. A full restage resets identity rather than
|
||||
* pretending a savestate is an original texture upload. */
|
||||
void gpu_hd_textures_set_vram(const uint16_t* vram);
|
||||
void gpu_hd_textures_track_upload(int x, int y, int width_words, int height,
|
||||
const uint16_t* words);
|
||||
void gpu_hd_textures_invalidate(int x, int y, int width_words, int height);
|
||||
void gpu_hd_textures_reset_tracking(void);
|
||||
|
||||
typedef struct GpuHdTextureImage {
|
||||
const uint8_t* rgba;
|
||||
uint32_t width, height, stride;
|
||||
uint64_t cache_key;
|
||||
uint64_t generation;
|
||||
int32_t origin_u, origin_v;
|
||||
uint32_t source_width, source_height;
|
||||
/* 1: Beetle (native STP), 2: Duck non-ST, 3: Duck ST alpha encoding. */
|
||||
int alpha_mode;
|
||||
void* lease;
|
||||
} GpuHdTextureImage;
|
||||
|
||||
/* Inclusive page-UV bounds, before native raster writes the destination.
|
||||
* Texture-window masks are applied to the queried footprint. Pending decode,
|
||||
* ambiguity and unsupported geometry all return 0 for native sampling. */
|
||||
int gpu_hd_textures_acquire_draw(uint16_t texpage, uint16_t clut_x,
|
||||
uint16_t clut_y, const int limits[4],
|
||||
uint32_t texture_window, int semitransparent,
|
||||
GpuHdTextureImage* image);
|
||||
void gpu_hd_textures_release_image(GpuHdTextureImage* image);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -102,6 +102,9 @@ int gr_render_display_hires(uint32_t *out_pixels, int out_pitch,
|
||||
int disp_x, int disp_y, int disp_w, int disp_h);
|
||||
|
||||
/* VRAM transfers */
|
||||
/* A0 header invalidation precedes payload writes; an interrupted transfer
|
||||
* cannot leave a prior immutable texture identity resident. */
|
||||
void gr_vram_upload_begin(int x, int y, int w, int h);
|
||||
void gr_vram_write(int x, int y, uint16_t pixel);
|
||||
uint16_t gr_vram_read(int x, int y);
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data);
|
||||
|
||||
@@ -328,6 +328,11 @@ void psx_mod_set_world_scene_predicate(PSXModWorldScenePredicate predicate);
|
||||
*/
|
||||
int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
const char* option_id, char* out, uint32_t out_size);
|
||||
/* Read an option of the package/feature whose trusted callback is running.
|
||||
* Shared framework plugins use this without hard-coding a title package id.
|
||||
* The same committed-plan and buffer rules as psx_mod_option_value apply. */
|
||||
int psx_mod_current_option_value(const char* option_id,
|
||||
char* out, uint32_t out_size);
|
||||
/*
|
||||
* Read the committed owner-selected path for a resource declared by the
|
||||
* package feature whose trusted plugin is currently running. Returns 0 when
|
||||
@@ -336,6 +341,15 @@ int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
*/
|
||||
int psx_mod_current_resource_path(const char* resource_id,
|
||||
char* out, uint32_t out_size);
|
||||
/* Configure the host HD texture pack from this callback's directory resource.
|
||||
* Replacements are currently OpenGL-only; dumping also works on software and
|
||||
* Vulkan. Configuration is host state, never guest RAM or savestate data.
|
||||
* Activation/emulation-thread only. Failure is reported to the runtime log. */
|
||||
int psx_mod_set_hd_texture_pack(const char* resource_id,
|
||||
int replacements_enabled, int dump_enabled);
|
||||
/* Emulation-thread controls for an already configured pack. */
|
||||
int psx_mod_set_hd_texture_dump(int enabled);
|
||||
int psx_mod_reload_hd_texture_pack(void);
|
||||
/* Read-only canonical media verified by the engine for this plugin's owning
|
||||
* package/feature. The pointer lives until the committed plan is replaced or
|
||||
* cleared. Available only during that plugin's callbacks; returns 0 for an
|
||||
@@ -775,6 +789,14 @@ typedef struct PSXModControllerInput {
|
||||
} PSXModControllerInput;
|
||||
typedef uint32_t (*PSXModControllerPresentationCallback)(
|
||||
const PSXModControllerInput* input);
|
||||
/* Read-only snapshot of the local host stick state for an emulation-thread
|
||||
* presentation callback. Values are ordered lx, ly, rx, ry and use the
|
||||
* DualShock byte range (0..255, centered at 128). This reads mapped local
|
||||
* controller axes independently of the guest's current digital/analog SIO
|
||||
* mode and never changes the simulation's controller sample. Returns 0 and
|
||||
* centers the output for invalid/disconnected input, netplay, or rollback
|
||||
* resimulation; presentation state must never follow synchronized peer input. */
|
||||
int psx_mod_read_local_pad_sticks(uint32_t player, uint8_t out[4]);
|
||||
/*
|
||||
* Override one player's resolved controller presentation mode for this launch.
|
||||
* This is intentionally a trusted-plugin API, not a generic launcher setting.
|
||||
|
||||
@@ -149,6 +149,10 @@ int pgxp_mod_request_take(int *cpu_mode, int *culling);
|
||||
/* Drop all shadows (savestate load, raw RAM restore, timeline breaks).
|
||||
* O(1) via generation bump. Deferred while suppressed. */
|
||||
void pgxp_invalidate_all(void);
|
||||
/* A DMA or host store has no instruction hook. Even an unchanged word can
|
||||
* belong to a different sub-pixel projection, so drop the shadow of the
|
||||
* touched RAM / scratchpad word by provenance, not by value (memory.c). */
|
||||
void pgxp_invalidate_word(uint32_t addr);
|
||||
|
||||
/* Counted suppression bracket for speculative native-validation passes and
|
||||
* the GTE replay sandbox: hooks and producers no-op inside it. */
|
||||
|
||||
@@ -51,7 +51,8 @@ struct CPUState;
|
||||
* load: rt <- [addr]; value = loaded word/half/byte (post-extension)
|
||||
* store: [addr] <- rt; value = the stored word/half/byte
|
||||
* alu: rd/rt <- op(s1, s2); result = the written value (also LUI, HILO
|
||||
* moves and MOVE idioms — the body decodes the class from instr)
|
||||
* moves, MOVE idioms and MFC0/CFC0 — the body decodes the class
|
||||
* from instr)
|
||||
* muldiv: HI/LO <- op(s1, s2)
|
||||
* cop2: MFC2/CFC2/MTC2/CTC2 (addr = 0) and LWC2/SWC2 (addr = guest addr);
|
||||
* value = the transferred word
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
xxHash Library
|
||||
Copyright (c) 2012-2021 Yann Collet
|
||||
All rights reserved.
|
||||
|
||||
Vendored header: xxHash v0.8.3, xxhash.h
|
||||
Copyright (C) 2012-2023 Yann Collet
|
||||
Source: https://github.com/Cyan4973/xxHash/blob/v0.8.3/xxhash.h
|
||||
SHA-256: 17973c0dc49d9854ca26caa191f0e12f7a424b68858d9a78de3860d959d85e4b
|
||||
The root LICENSE and the header both grant the BSD 2-Clause terms below.
|
||||
|
||||
BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -365,6 +365,9 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_vram_dirty.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_render.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_hd_textures.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/hd_texture_pack.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/duckstation_texture_pack.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_openxr.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/pad_external_input.c
|
||||
@@ -459,6 +462,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_pgxp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_bezel.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_ram.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_hd_textures.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_media.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#endif
|
||||
#include "dma.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "gpu_render.h" /* gr_scale + gr_render_display_hires (screenshot_hires) */
|
||||
#include "present_ring.h"
|
||||
#include "gpu_timeline.h"
|
||||
@@ -9916,6 +9917,56 @@ encode:;
|
||||
* surface was actually allocated at (after the driver/memory clamp), the
|
||||
* driver limit, the hr surface size, and the window's point and pixel sizes
|
||||
* (the HiDPI drawable). Everything a preset needs to be verified against. */
|
||||
static void handle_hd_textures(int id, const char *json)
|
||||
{
|
||||
GpuHdTextureDiag info;
|
||||
gpu_hd_textures_get_diag(&info);
|
||||
/* Development validation controls run at the emulation-thread boundary.
|
||||
* The normal owner workflow remains the Mods controls applied on Play. */
|
||||
const int replacements = json_get_int(json, "replacements", -1);
|
||||
const int dump = json_get_int(json, "dump", -1);
|
||||
const int reload = json_get_int(json, "reload", 0);
|
||||
if (replacements != -1 || dump != -1 || reload) {
|
||||
if ((replacements < -1 || replacements > 1) || (dump < -1 || dump > 1)) {
|
||||
send_err(id, "replacements and dump must be 0 or 1"); return;
|
||||
}
|
||||
if (!info.root || !info.root[0]) {
|
||||
send_err(id, "no texture pack is configured; enable the mod and click Play"); return;
|
||||
}
|
||||
char error[512] = "";
|
||||
if (replacements != -1) {
|
||||
if (!gpu_hd_textures_configure(info.root, replacements,
|
||||
dump == -1 ? info.dump : dump, error, sizeof(error))) {
|
||||
send_err(id, error); return;
|
||||
}
|
||||
} else {
|
||||
if (dump != -1) gpu_hd_textures_set_dump_enabled(dump);
|
||||
if (reload && !gpu_hd_textures_reload(error, sizeof(error))) {
|
||||
send_err(id, error); return;
|
||||
}
|
||||
}
|
||||
gpu_hd_textures_get_diag(&info);
|
||||
}
|
||||
char root_json[8192];
|
||||
json_escape_string(root_json, sizeof(root_json), info.root ? info.root : "");
|
||||
const int backend = gr_backend();
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"root\":\"%s\",\"active\":%s,"
|
||||
"\"replacements\":%s,\"dump\":%s,\"format\":%d,"
|
||||
"\"backend\":\"%s\",\"replacement_backend_supported\":%s,"
|
||||
"\"replacement_count\":%llu,\"draw_queries\":%llu,"
|
||||
"\"matched_draws\":%llu,\"ready_draws\":%llu,"
|
||||
"\"applied_draws\":%llu,\"dumped_textures\":%llu}",
|
||||
id, root_json, info.active ? "true" : "false",
|
||||
info.replacements ? "true" : "false", info.dump ? "true" : "false", info.format,
|
||||
backend == GR_BACKEND_OPENGL ? "opengl" :
|
||||
backend == GR_BACKEND_VULKAN ? "vulkan" : "software",
|
||||
backend == GR_BACKEND_OPENGL ? "true" : "false",
|
||||
(unsigned long long)info.replacement_count,
|
||||
(unsigned long long)info.draw_queries, (unsigned long long)info.matched_draws,
|
||||
(unsigned long long)info.ready_draws, (unsigned long long)info.applied_draws,
|
||||
(unsigned long long)info.dumped_textures);
|
||||
}
|
||||
|
||||
static void handle_video_info(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -15484,6 +15535,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "dump_buffer", handle_dump_buffer },
|
||||
{ "wide_full", handle_wide_full },
|
||||
{ "video_info", handle_video_info },
|
||||
{ "hd_textures", handle_hd_textures },
|
||||
{ "screenshot_wide_hires", handle_screenshot_wide_hires },
|
||||
{ "wide_shot", handle_wide_shot },
|
||||
{ "gpu_opcodes", handle_gpu_opcodes },
|
||||
|
||||
@@ -2238,6 +2238,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->ld_which_t = (uint8_t)rt;
|
||||
#endif
|
||||
cpu->gpr[rt] = cop0_read;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rt], 0, 0);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2247,6 +2248,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->ld_which_t = (uint8_t)rt;
|
||||
#endif
|
||||
cpu->gpr[rt] = cop0_read;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rt], 0, 0);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,751 @@
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "../third_party/stb_image.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "../third_party/xxhash.h"
|
||||
#include "png_write.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
constexpr size_t kWords = 1024u * 512u;
|
||||
constexpr size_t kMaxEntries = 262144;
|
||||
constexpr size_t kMaxUploads = 8192;
|
||||
constexpr size_t kMaxRectCache = 256;
|
||||
constexpr size_t kMaxDumps = 8192;
|
||||
constexpr size_t kMaxDumpQueue = 8;
|
||||
constexpr size_t kMaxDecodeQueue = 32;
|
||||
constexpr size_t kDefaultBudget = 64u * 1024u * 1024u;
|
||||
constexpr size_t kMaxEncoded = 64u * 1024u * 1024u;
|
||||
constexpr uint32_t kMaxImageDimension = 8192;
|
||||
|
||||
void error_text(char* dest, size_t capacity, const std::string& text) {
|
||||
if (dest && capacity) std::snprintf(dest, capacity, "%s", text.c_str());
|
||||
}
|
||||
std::string lower(std::string s) {
|
||||
for (char& c : s) if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
|
||||
return s;
|
||||
}
|
||||
std::string trim(const std::string& s) {
|
||||
const auto a = s.find_first_not_of(" \t\r\n");
|
||||
if (a == std::string::npos) return {};
|
||||
return s.substr(a, s.find_last_not_of(" \t\r\n") - a + 1);
|
||||
}
|
||||
unsigned expansion(unsigned depth) { return depth == 0 ? 4 : depth == 1 ? 2 : 1; }
|
||||
struct Rect { unsigned x, y, w, h; };
|
||||
bool intersects(Rect a, Rect b) {
|
||||
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
||||
}
|
||||
bool contains(Rect a, Rect b) {
|
||||
return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
|
||||
}
|
||||
bool valid_rect(Rect r) { return r.w && r.h && r.x + r.w <= 1024 && r.y + r.h <= 512; }
|
||||
|
||||
template<class T> bool number(const std::string& text, T& value, int base = 10) {
|
||||
if (text.empty()) return false;
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), value, base);
|
||||
return result.ec == std::errc{} && result.ptr == text.data() + text.size();
|
||||
}
|
||||
bool dimensions(const std::string& s, uint16_t& w, uint16_t& h) {
|
||||
const size_t at = s.find('x');
|
||||
return at != std::string::npos && number(s.substr(0, at), w) &&
|
||||
number(s.substr(at + 1), h) && w && h;
|
||||
}
|
||||
bool valid_key(const DuckTextureKey& k) {
|
||||
if (k.kind > DUCK_TEXTURE_PAGE || k.depth > 2 || k.semitransparent > 1 ||
|
||||
!k.source_width_words || k.source_width_words > 1024 || !k.source_height ||
|
||||
k.source_height > 512 || !k.width || !k.height) return false;
|
||||
const unsigned ppw = expansion(k.depth);
|
||||
if (k.offset_x % ppw || k.width % ppw ||
|
||||
unsigned(k.offset_x) + k.width > unsigned(k.source_width_words) * ppw ||
|
||||
unsigned(k.offset_y) + k.height > k.source_height) return false;
|
||||
if (k.kind == DUCK_TEXTURE_PAGE && (k.source_width_words != 256 / ppw ||
|
||||
k.source_height != 256 || unsigned(k.offset_x) + k.width > 256 ||
|
||||
unsigned(k.offset_y) + k.height > 256)) return false;
|
||||
if (k.depth < 2 && (k.palette_min > k.palette_max ||
|
||||
k.palette_max >= (k.depth == 0 ? 16 : 256))) return false;
|
||||
return k.depth < 2 || (!k.palette_hash && !k.palette_min && !k.palette_max);
|
||||
}
|
||||
std::string stem(const DuckTextureKey& k) {
|
||||
char result[192];
|
||||
if (!duck_texture_format_name(&k, result, sizeof(result))) return {};
|
||||
return result;
|
||||
}
|
||||
bool query_rect(const HdTextureDrawQuery* q, Rect& r) {
|
||||
if (!q || !q->vram || q->vram_word_count < kWords || q->depth > 2 ||
|
||||
q->u_first > q->u_last || q->v_first > q->v_last ||
|
||||
q->page_x >= 1024 || q->page_x % 64 || q->page_y > 256 || q->page_y % 256)
|
||||
return false;
|
||||
const unsigned ppw = expansion(q->depth);
|
||||
/* A page that wraps in X is outside the initial safe subset. */
|
||||
if (q->page_x + 256 / ppw > 1024) return false;
|
||||
r = {q->page_x + q->u_first / ppw, unsigned(q->page_y) + q->v_first,
|
||||
unsigned(q->u_last / ppw - q->u_first / ppw + 1),
|
||||
unsigned(q->v_last - q->v_first + 1)};
|
||||
return valid_rect(r);
|
||||
}
|
||||
|
||||
uint64_t palette_hash(const HdTextureDrawQuery& q, unsigned first, unsigned last, bool& valid) {
|
||||
valid = false;
|
||||
if (q.depth == 2) { valid = true; return 0; }
|
||||
const unsigned full = q.depth == 0 ? 16 : 256;
|
||||
if (first > last || last >= full || q.clut_x >= 1024 || q.clut_y >= 512 || q.clut_x % 16)
|
||||
return 0;
|
||||
unsigned count = last - first + 1;
|
||||
if (first == 0 && last == full - 1) count = std::min(full, 1024u - q.clut_x);
|
||||
else if (q.clut_x + last >= 1024) return 0;
|
||||
/* Compatibility with the pinned upstream behavior: range length is used,
|
||||
* but min does not offset the hashed palette pointer. */
|
||||
valid = true;
|
||||
return duck_texture_hash_words_le(q.vram + q.clut_y * 1024 + q.clut_x, count);
|
||||
}
|
||||
|
||||
struct Entry { DuckTextureKey key{}; std::string path; bool ambiguous = false; };
|
||||
struct Upload { Rect rect; uint64_t hash; };
|
||||
struct RectHash { Rect rect; uint64_t hash; const uint16_t* vram; };
|
||||
struct PageGroup {
|
||||
Rect texels;
|
||||
std::unordered_map<uint64_t, std::vector<size_t>> hashes;
|
||||
};
|
||||
struct MatchCache {
|
||||
HdTextureDrawQuery query;
|
||||
int st, status;
|
||||
DuckTextureMatch result;
|
||||
};
|
||||
struct Image { std::vector<uint8_t> rgba; uint32_t w = 0, h = 0; };
|
||||
struct Lease { std::shared_ptr<Image> image; };
|
||||
enum class DecodeStatus { Queued, Loading, Ready, Failed };
|
||||
struct Decode {
|
||||
DecodeStatus status = DecodeStatus::Queued;
|
||||
std::string path;
|
||||
std::shared_ptr<Image> image;
|
||||
uint64_t use = 0;
|
||||
};
|
||||
struct Dump { fs::path path; std::shared_ptr<Image> image; };
|
||||
/* png_write.h's C-compatible CRC table is mutable during initialization.
|
||||
* Different pack workers therefore serialize this TU's calls to the writer. */
|
||||
std::mutex png_writer_mutex;
|
||||
std::atomic<uint64_t> dump_sequence{static_cast<uint64_t>(std::chrono::steady_clock::now().time_since_epoch().count())};
|
||||
|
||||
FILE* create_exclusive(const fs::path& path) {
|
||||
#ifdef _WIN32
|
||||
HANDLE handle = CreateFileW(path.c_str(),GENERIC_WRITE,0,nullptr,CREATE_NEW,FILE_ATTRIBUTE_NORMAL,nullptr);
|
||||
if (handle == INVALID_HANDLE_VALUE) return nullptr;
|
||||
const int fd = _open_osfhandle(reinterpret_cast<intptr_t>(handle),_O_BINARY | _O_WRONLY);
|
||||
if (fd < 0) { CloseHandle(handle); return nullptr; }
|
||||
FILE* file = _fdopen(fd,"wb");
|
||||
if (!file) _close(fd);
|
||||
#else
|
||||
const int fd = ::open(path.c_str(),O_WRONLY | O_CREAT | O_EXCL,0600);
|
||||
if (fd < 0) return nullptr;
|
||||
FILE* file = ::fdopen(fd,"wb");
|
||||
if (!file) ::close(fd);
|
||||
#endif
|
||||
return file;
|
||||
}
|
||||
|
||||
bool publish_exclusive(const fs::path& temp, const fs::path& target) {
|
||||
#ifdef _WIN32
|
||||
if (MoveFileExW(temp.c_str(),target.c_str(),0)) return true;
|
||||
const DWORD code = GetLastError();
|
||||
if (code != ERROR_ALREADY_EXISTS && code != ERROR_FILE_EXISTS) throw std::runtime_error("cannot publish texture dump PNG");
|
||||
#else
|
||||
if (::link(temp.c_str(),target.c_str()) == 0) return true;
|
||||
if (errno != EEXIST) throw std::runtime_error("cannot publish texture dump PNG");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
std::mutex mutex;
|
||||
std::condition_variable wake;
|
||||
std::unordered_map<uint64_t, Decode> decoded;
|
||||
std::deque<uint64_t> decode_queue;
|
||||
std::deque<Dump> dump_queue;
|
||||
std::thread thread;
|
||||
size_t budget = kDefaultBudget, used = 0;
|
||||
uint64_t tick = 0;
|
||||
bool stop = false;
|
||||
std::string dump_error;
|
||||
~Worker() {
|
||||
{ std::lock_guard<std::mutex> lock(mutex); stop = true; }
|
||||
wake.notify_all();
|
||||
if (thread.joinable()) thread.join();
|
||||
}
|
||||
void evict(size_t incoming) {
|
||||
while (used + incoming > budget) {
|
||||
auto candidate = decoded.end();
|
||||
for (auto it = decoded.begin(); it != decoded.end(); ++it)
|
||||
if (it->second.status == DecodeStatus::Ready &&
|
||||
(candidate == decoded.end() || it->second.use < candidate->second.use)) candidate = it;
|
||||
if (candidate == decoded.end()) break;
|
||||
used -= candidate->second.image->rgba.size();
|
||||
decoded.erase(candidate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void worker_main(Worker* w) {
|
||||
#ifdef _WIN32
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
#endif
|
||||
for (;;) {
|
||||
uint64_t id = 0; std::string path; Dump dump; size_t budget = 0;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(w->mutex);
|
||||
w->wake.wait(lock, [&] { return w->stop || !w->decode_queue.empty() || !w->dump_queue.empty(); });
|
||||
/* Finish queued dumps on shutdown so the final frame is durable. */
|
||||
if (w->stop && w->dump_queue.empty()) return;
|
||||
if (!w->stop && !w->decode_queue.empty()) {
|
||||
id = w->decode_queue.front(); w->decode_queue.pop_front();
|
||||
auto it = w->decoded.find(id);
|
||||
if (it == w->decoded.end()) continue;
|
||||
it->second.status = DecodeStatus::Loading;
|
||||
path = it->second.path; budget = w->budget;
|
||||
} else { dump = std::move(w->dump_queue.front()); w->dump_queue.pop_front(); }
|
||||
}
|
||||
if (id) {
|
||||
std::shared_ptr<Image> image;
|
||||
try {
|
||||
std::ifstream input(fs::u8path(path), std::ios::binary | std::ios::ate);
|
||||
const auto length = input ? input.tellg() : std::streampos(-1);
|
||||
if (length > 0 && static_cast<uint64_t>(length) <= kMaxEncoded) {
|
||||
std::vector<uint8_t> encoded(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
int width = 0, height = 0, channels = 0;
|
||||
constexpr uint8_t png_signature[] = {137,80,78,71,13,10,26,10};
|
||||
if (input.read(reinterpret_cast<char*>(encoded.data()), length) &&
|
||||
encoded.size() >= sizeof(png_signature) &&
|
||||
!std::memcmp(encoded.data(),png_signature,sizeof(png_signature)) &&
|
||||
stbi_info_from_memory(encoded.data(), static_cast<int>(encoded.size()), &width, &height, &channels) &&
|
||||
width > 0 && height > 0 && width <= int(kMaxImageDimension) && height <= int(kMaxImageDimension) &&
|
||||
uint64_t(width) * height * 4 <= budget) {
|
||||
stbi_uc* pixels = stbi_load_from_memory(encoded.data(), static_cast<int>(encoded.size()),
|
||||
&width, &height, &channels, 4);
|
||||
if (pixels) {
|
||||
std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> owned(pixels, &stbi_image_free);
|
||||
image = std::make_shared<Image>(); image->w = width; image->h = height;
|
||||
image->rgba.assign(pixels, pixels + size_t(width) * height * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (...) { image.reset(); }
|
||||
std::lock_guard<std::mutex> lock(w->mutex);
|
||||
auto it = w->decoded.find(id);
|
||||
if (it == w->decoded.end()) continue;
|
||||
if (image) w->evict(image->rgba.size());
|
||||
if (!image || w->used + image->rgba.size() > w->budget) { it->second.status = DecodeStatus::Failed; continue; }
|
||||
w->used += image->rgba.size(); it->second.image = std::move(image);
|
||||
it->second.status = DecodeStatus::Ready; it->second.use = ++w->tick;
|
||||
} else {
|
||||
bool created = false;
|
||||
fs::path temporary = dump.path;
|
||||
temporary += ".partial-" + std::to_string(++dump_sequence);
|
||||
try {
|
||||
std::error_code ec;
|
||||
fs::create_directories(dump.path.parent_path(), ec);
|
||||
if (ec) throw std::runtime_error("cannot create texture dump directory: " + ec.message());
|
||||
if (fs::exists(dump.path,ec) && !ec) continue;
|
||||
FILE* file = create_exclusive(temporary);
|
||||
if (!file) throw std::runtime_error("cannot create temporary texture dump PNG");
|
||||
created = true;
|
||||
bool success = false;
|
||||
try {
|
||||
std::lock_guard<std::mutex> write_lock(png_writer_mutex);
|
||||
success = png_write_rgba(file,dump.image->rgba.data(),dump.image->w,dump.image->h) != 0;
|
||||
} catch (...) { std::fclose(file); throw; }
|
||||
if (std::fclose(file) != 0 || !success) throw std::runtime_error("cannot write texture dump PNG");
|
||||
publish_exclusive(temporary,dump.path);
|
||||
fs::remove(temporary,ec);
|
||||
} catch (const std::exception& e) {
|
||||
if (created) { std::error_code ignored; fs::remove(temporary,ignored); }
|
||||
std::lock_guard<std::mutex> lock(w->mutex); w->dump_error = e.what();
|
||||
std::fprintf(stderr,"[texture-pack] dump failed: %s\n",e.what());
|
||||
} catch (...) {
|
||||
if (created) { std::error_code ignored; fs::remove(temporary,ignored); }
|
||||
std::lock_guard<std::mutex> lock(w->mutex); w->dump_error = "texture dump allocation failed";
|
||||
std::fprintf(stderr,"[texture-pack] dump failed: allocation failed\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct DuckTexturePack {
|
||||
std::string root, diagnostic;
|
||||
fs::path replacements, dumps;
|
||||
std::vector<Entry> entries;
|
||||
std::array<std::unordered_map<uint64_t, std::vector<size_t>>, 3> uploads_by_hash;
|
||||
std::array<std::unordered_map<uint64_t, PageGroup>, 3> page_groups;
|
||||
std::unordered_map<std::string, size_t> by_name;
|
||||
std::vector<Upload> uploads;
|
||||
std::vector<RectHash> rect_cache;
|
||||
std::vector<MatchCache> match_cache;
|
||||
std::unordered_set<std::string> dumped;
|
||||
size_t ignored = 0, ambiguous = 0;
|
||||
bool geometry_limit_reported = false, dump_limit_reported = false;
|
||||
Worker worker;
|
||||
};
|
||||
|
||||
namespace {
|
||||
void note(DuckTexturePack& p, const std::string& s) {
|
||||
++p.ignored;
|
||||
if (p.diagnostic.size() < 2048) p.diagnostic += (p.diagnostic.empty() ? "" : "; ") + s;
|
||||
}
|
||||
void add_entry(DuckTexturePack& p, const DuckTextureKey& key, const fs::path& path) {
|
||||
const std::string name = stem(key);
|
||||
auto previous = p.by_name.find(name);
|
||||
if (previous != p.by_name.end()) {
|
||||
auto& entry = p.entries[previous->second];
|
||||
if (!entry.ambiguous) { entry.ambiguous = true; ++p.ambiguous; }
|
||||
return;
|
||||
}
|
||||
if (p.entries.size() >= kMaxEntries) throw std::runtime_error("texture replacement entry limit (262144) exceeded");
|
||||
p.by_name.emplace(name, p.entries.size());
|
||||
if (key.kind == DUCK_TEXTURE_UPLOAD) p.uploads_by_hash[key.depth][key.source_hash].push_back(p.entries.size());
|
||||
else {
|
||||
const uint64_t geometry = (uint64_t(key.offset_x) << 48) | (uint64_t(key.offset_y) << 32) |
|
||||
(uint64_t(key.width) << 16) | key.height;
|
||||
PageGroup& group = p.page_groups[key.depth][geometry];
|
||||
group.texels = {key.offset_x, key.offset_y, key.width, key.height};
|
||||
group.hashes[key.source_hash].push_back(p.entries.size());
|
||||
}
|
||||
p.entries.push_back({key, path.u8string(), false});
|
||||
}
|
||||
uint64_t cached_hash(DuckTexturePack& p, const HdTextureDrawQuery& q, Rect r) {
|
||||
for (const auto& saved : p.rect_cache)
|
||||
if (saved.vram == q.vram && saved.rect.x == r.x && saved.rect.y == r.y && saved.rect.w == r.w && saved.rect.h == r.h) return saved.hash;
|
||||
const uint64_t value = duck_texture_hash_rect(q.vram, q.vram_word_count, r.x, r.y, r.w, r.h);
|
||||
if (p.rect_cache.size() >= kMaxRectCache) p.rect_cache.erase(p.rect_cache.begin());
|
||||
p.rect_cache.push_back({r, value, q.vram});
|
||||
return value;
|
||||
}
|
||||
std::string unquote(std::string s) {
|
||||
s = trim(s);
|
||||
if (s.size() > 1 && ((s.front() == '\'' && s.back() == '\'') || (s.front() == '"' && s.back() == '"')))
|
||||
return s.substr(1, s.size() - 2);
|
||||
const auto comment = s.find(" #");
|
||||
return comment == std::string::npos ? s : trim(s.substr(0, comment));
|
||||
}
|
||||
void load_config(DuckTexturePack& p, const fs::path& path) {
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec)) return;
|
||||
if (fs::file_size(path, ec) > 1024 * 1024 || ec) { note(p, "config.yaml exceeds 1 MiB"); return; }
|
||||
std::ifstream file(path); std::string line; bool aliases = false;
|
||||
while (std::getline(file, line)) {
|
||||
const std::string text = trim(line);
|
||||
if (text.empty() || text.front() == '#') continue;
|
||||
if (line.size() > 4096) { note(p, "oversized config.yaml line"); continue; }
|
||||
const auto colon = text.find(':');
|
||||
if (colon == std::string::npos) { note(p, "unsupported config.yaml syntax"); continue; }
|
||||
const std::string key = unquote(text.substr(0, colon));
|
||||
std::string value = unquote(text.substr(colon + 1));
|
||||
const bool indented = line.front() == ' ' || line.front() == '\t';
|
||||
if (!indented) { aliases = key == "Aliases"; if (!aliases) note(p, "config.yaml option not applied: " + key); continue; }
|
||||
if (!aliases) continue;
|
||||
DuckTextureKey parsed{};
|
||||
if (!duck_texture_parse_name(key.c_str(), &parsed)) { note(p, "invalid alias key"); continue; }
|
||||
if (value == "|" || value == "|-" || value == ">" || value == ">-") {
|
||||
if (!std::getline(file, line) || line.find_first_not_of(" \t") < 4) { note(p, "unsupported alias block"); continue; }
|
||||
value = trim(line);
|
||||
}
|
||||
fs::path relative = fs::u8path(value);
|
||||
bool unsafe = relative.empty() || relative.is_absolute() || relative.has_root_name();
|
||||
for (const auto& part : relative) if (part == "..") unsafe = true;
|
||||
if (unsafe || lower(relative.extension().string()) != ".png") { note(p, "unsupported alias path"); continue; }
|
||||
const fs::path target = p.replacements / relative;
|
||||
if (!fs::is_regular_file(target, ec) || ec) { note(p, "alias target missing"); ec.clear(); continue; }
|
||||
const auto old = p.by_name.find(stem(parsed));
|
||||
/* A direct canonical filename takes precedence over an alias. */
|
||||
if (old == p.by_name.end()) add_entry(p, parsed, target);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
int duck_texture_parse_name(const char* filename, DuckTextureKey* out) {
|
||||
if (!filename || !out) return 0;
|
||||
try {
|
||||
std::string text(filename);
|
||||
if (text.size() > 192 || text.find_first_of("/\\") != std::string::npos) return 0;
|
||||
const auto dot = text.find('.');
|
||||
if (dot != std::string::npos) {
|
||||
if (lower(text.substr(dot)) != ".png") return 0;
|
||||
text.resize(dot);
|
||||
}
|
||||
std::vector<std::string> parts;
|
||||
size_t begin = 0;
|
||||
for (size_t at = 0; at <= text.size(); ++at) if (at == text.size() || text[at] == '-') {
|
||||
parts.push_back(text.substr(begin, at - begin)); begin = at + 1;
|
||||
}
|
||||
if (parts.size() != 7 && parts.size() != 10) return 0;
|
||||
DuckTextureKey k{};
|
||||
if (parts[0] == "texupload") k.kind = DUCK_TEXTURE_UPLOAD;
|
||||
else if (parts[0] == "texpage") k.kind = DUCK_TEXTURE_PAGE;
|
||||
else return 0;
|
||||
std::string mode = parts[1];
|
||||
if (mode.size() > 2 && mode.substr(0, 2) == "ST") { k.semitransparent = 1; mode.erase(0, 2); }
|
||||
if (mode == "P4") k.depth = 0; else if (mode == "P8") k.depth = 1; else if (mode == "C16") k.depth = 2; else return 0;
|
||||
if (parts[2].size() != 16 || !number(parts[2], k.source_hash, 16)) return 0;
|
||||
size_t field = 3;
|
||||
if (k.depth < 2) {
|
||||
if (parts.size() != 10 || parts[3].size() != 16 || !number(parts[3], k.palette_hash, 16)) return 0;
|
||||
++field;
|
||||
} else if (parts.size() != 7) return 0;
|
||||
if (!dimensions(parts[field++], k.source_width_words, k.source_height) ||
|
||||
!number(parts[field++], k.offset_x) || !number(parts[field++], k.offset_y) ||
|
||||
!dimensions(parts[field++], k.width, k.height)) return 0;
|
||||
if (k.depth < 2) {
|
||||
unsigned a = 0, b = 0;
|
||||
if (parts[field].empty() || parts[field][0] != 'P' || !number(parts[field].substr(1), a) ||
|
||||
!number(parts[field + 1], b) || a > 255 || b > 255) return 0;
|
||||
k.palette_min = static_cast<uint8_t>(a); k.palette_max = static_cast<uint8_t>(b);
|
||||
}
|
||||
if (!valid_key(k)) return 0;
|
||||
*out = k; return 1;
|
||||
} catch (...) { return 0; }
|
||||
}
|
||||
|
||||
int duck_texture_format_name(const DuckTextureKey* k, char* out, size_t capacity) {
|
||||
if (!k || !out || !capacity || !valid_key(*k)) return 0;
|
||||
const char* kind = k->kind == DUCK_TEXTURE_UPLOAD ? "texupload" : "texpage";
|
||||
const char* mode = k->depth == 0 ? "P4" : k->depth == 1 ? "P8" : "C16";
|
||||
const char* st = k->semitransparent ? "ST" : "";
|
||||
int count;
|
||||
if (k->depth < 2) count = std::snprintf(out, capacity,
|
||||
"%s-%s%s-%016llX-%016llX-%ux%u-%u-%u-%ux%u-P%u-%u", kind, st, mode,
|
||||
static_cast<unsigned long long>(k->source_hash), static_cast<unsigned long long>(k->palette_hash),
|
||||
k->source_width_words, k->source_height, k->offset_x, k->offset_y, k->width, k->height, k->palette_min, k->palette_max);
|
||||
else count = std::snprintf(out, capacity, "%s-%s%s-%016llX-%ux%u-%u-%u-%ux%u", kind, st, mode,
|
||||
static_cast<unsigned long long>(k->source_hash), k->source_width_words, k->source_height,
|
||||
k->offset_x, k->offset_y, k->width, k->height);
|
||||
return count >= 0 && static_cast<size_t>(count) < capacity;
|
||||
}
|
||||
|
||||
uint64_t duck_texture_hash_words_le(const uint16_t* words, size_t count) {
|
||||
if ((!words && count) || count > kWords) return 0;
|
||||
XXH3_state_t state;
|
||||
XXH3_64bits_reset(&state);
|
||||
std::array<uint8_t, 2048> bytes{};
|
||||
while (count) {
|
||||
const size_t chunk = std::min(count, bytes.size() / 2);
|
||||
for (size_t i = 0; i < chunk; ++i) { bytes[2 * i] = words[i] & 255; bytes[2 * i + 1] = words[i] >> 8; }
|
||||
XXH3_64bits_update(&state, bytes.data(), chunk * 2);
|
||||
words += chunk; count -= chunk;
|
||||
}
|
||||
return XXH3_64bits_digest(&state);
|
||||
}
|
||||
uint64_t duck_texture_hash_rect(const uint16_t* vram, size_t count, uint16_t x, uint16_t y,
|
||||
uint16_t width, uint16_t height) {
|
||||
if (!vram || count < kWords || !valid_rect({x, y, width, height})) return 0;
|
||||
XXH3_state_t state; XXH3_64bits_reset(&state);
|
||||
std::array<uint8_t, 2048> bytes{};
|
||||
for (unsigned row = 0; row < height; ++row) {
|
||||
const uint16_t* src = vram + (y + row) * 1024 + x;
|
||||
for (unsigned i = 0; i < width; ++i) { bytes[2 * i] = src[i] & 255; bytes[2 * i + 1] = src[i] >> 8; }
|
||||
XXH3_64bits_update(&state, bytes.data(), size_t(width) * 2);
|
||||
}
|
||||
return XXH3_64bits_digest(&state);
|
||||
}
|
||||
|
||||
int duck_texture_pack_create(const char* root, DuckTexturePack** out, char* error, size_t capacity) {
|
||||
if (out) *out = nullptr;
|
||||
if (!out || !root || !root[0]) { error_text(error, capacity, "texture pack root is unset"); return 0; }
|
||||
try {
|
||||
auto p = std::make_unique<DuckTexturePack>();
|
||||
std::error_code ec; fs::path input = fs::absolute(fs::u8path(root), ec).lexically_normal();
|
||||
if (ec || (fs::exists(input, ec) && !fs::is_directory(input, ec))) {
|
||||
error_text(error, capacity, "texture pack root is not a directory"); return 0;
|
||||
}
|
||||
if (lower(input.filename().u8string()) == "replacements") { p->replacements = input; input = input.parent_path(); }
|
||||
else p->replacements = input / "replacements";
|
||||
p->root = input.u8string(); p->dumps = input / "dumps";
|
||||
std::vector<fs::path> paths;
|
||||
if (fs::is_directory(p->replacements, ec)) {
|
||||
size_t scanned = 0;
|
||||
for (fs::recursive_directory_iterator it(p->replacements, fs::directory_options::skip_permission_denied, ec), end;
|
||||
!ec && it != end; it.increment(ec)) {
|
||||
if (++scanned > kMaxEntries * 4) throw std::runtime_error("texture replacement directory scan limit exceeded");
|
||||
if (it.depth() >= 16) { if (it->is_directory(ec)) it.disable_recursion_pending(); note(*p, "replacement nesting exceeds 16 levels"); }
|
||||
if (it->is_regular_file(ec)) {
|
||||
const std::string name = it->path().filename().u8string();
|
||||
if (name.rfind("texupload-", 0) != 0 && name.rfind("texpage-", 0) != 0 && name.rfind("vram-write-", 0) != 0) continue;
|
||||
if (lower(it->path().extension().string()) != ".png") { note(*p, "replacement format outside PNG subset"); continue; }
|
||||
if (paths.size() >= kMaxEntries) throw std::runtime_error("texture replacement file limit (262144) exceeded");
|
||||
paths.push_back(it->path());
|
||||
}
|
||||
}
|
||||
if (ec) { error_text(error, capacity, "cannot scan replacement directory: " + ec.message()); return 0; }
|
||||
} else if (ec && ec != std::errc::no_such_file_or_directory) {
|
||||
error_text(error, capacity, "cannot inspect replacement directory: " + ec.message()); return 0;
|
||||
}
|
||||
std::sort(paths.begin(), paths.end());
|
||||
for (const auto& path : paths) {
|
||||
DuckTextureKey k{};
|
||||
if (duck_texture_parse_name(path.filename().u8string().c_str(), &k)) add_entry(*p, k, path);
|
||||
else note(*p, "invalid or unsupported replacement filename");
|
||||
}
|
||||
load_config(*p, input / "config.yaml");
|
||||
*out = p.release(); return 1;
|
||||
} catch (const std::exception& e) { error_text(error, capacity, e.what()); return 0; }
|
||||
catch (...) { error_text(error, capacity, "texture pack allocation failed"); return 0; }
|
||||
}
|
||||
void duck_texture_pack_destroy(DuckTexturePack* p) { delete p; }
|
||||
void duck_texture_pack_get_info(const DuckTexturePack* p, DuckTexturePackInfo* out) {
|
||||
if (!out) return;
|
||||
*out = {};
|
||||
if (p) { out->root = p->root.c_str(); out->diagnostic = p->diagnostic.c_str(); out->replacement_count = p->entries.size();
|
||||
out->ambiguous_count = p->ambiguous; out->ignored_count = p->ignored; }
|
||||
}
|
||||
void duck_texture_pack_invalidate(DuckTexturePack* p, uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||
if (!p || !w || !h) return;
|
||||
if (w > 1024 || h > 512) { duck_texture_pack_reset_tracking(p); return; }
|
||||
const unsigned px = x % 1024, py = y % 512;
|
||||
const unsigned w0 = std::min(unsigned(w), 1024 - px), h0 = std::min(unsigned(h), 512 - py);
|
||||
const std::array<Rect, 4> regions{{{px,py,w0,h0},{0,py,unsigned(w)-w0,h0},
|
||||
{px,0,w0,unsigned(h)-h0},{0,0,unsigned(w)-w0,unsigned(h)-h0}}};
|
||||
for (const auto& r : regions) if (r.w && r.h) {
|
||||
const size_t old_uploads = p->uploads.size();
|
||||
p->uploads.erase(std::remove_if(p->uploads.begin(), p->uploads.end(), [&](const Upload& u){ return intersects(u.rect,r); }), p->uploads.end());
|
||||
p->rect_cache.erase(std::remove_if(p->rect_cache.begin(), p->rect_cache.end(), [&](const RectHash& v){ return intersects(v.rect,r); }), p->rect_cache.end());
|
||||
if (p->uploads.size() != old_uploads) p->match_cache.clear();
|
||||
else p->match_cache.erase(std::remove_if(p->match_cache.begin(), p->match_cache.end(), [&](const MatchCache& c) {
|
||||
const auto& q = c.query;
|
||||
const Rect page{q.page_x,q.page_y,256 / expansion(q.depth),256};
|
||||
const Rect clut{q.clut_x,q.clut_y,std::min(q.depth == 0 ? 16u : 256u,1024u-q.clut_x),1};
|
||||
return intersects(page,r) || (q.depth < 2 && intersects(clut,r));
|
||||
}), p->match_cache.end());
|
||||
}
|
||||
}
|
||||
void duck_texture_pack_reset_tracking(DuckTexturePack* p) { if (p) { p->uploads.clear(); p->rect_cache.clear(); p->match_cache.clear(); } }
|
||||
int duck_texture_pack_copy_tracking(DuckTexturePack* destination, const DuckTexturePack* source) {
|
||||
if (!destination || !source) return 0;
|
||||
try {
|
||||
auto uploads = source->uploads;
|
||||
destination->uploads.swap(uploads);
|
||||
destination->rect_cache.clear();
|
||||
destination->match_cache.clear();
|
||||
return 1;
|
||||
} catch (...) { return 0; }
|
||||
}
|
||||
int duck_texture_pack_track_upload(DuckTexturePack* p, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
const uint16_t* vram, size_t count) {
|
||||
if (!p) return 0;
|
||||
duck_texture_pack_invalidate(p, x, y, w, h);
|
||||
if (!vram || count < kWords || !valid_rect({x,y,w,h})) return 0;
|
||||
try {
|
||||
if (p->uploads.size() >= kMaxUploads) p->uploads.erase(p->uploads.begin());
|
||||
p->uploads.push_back({{x,y,w,h},duck_texture_hash_rect(vram,count,x,y,w,h)});
|
||||
p->match_cache.clear(); return 1;
|
||||
} catch (...) { return 0; }
|
||||
}
|
||||
|
||||
int duck_texture_pack_match(DuckTexturePack* p, const HdTextureDrawQuery* q, DuckTextureMatch* out) {
|
||||
return duck_texture_pack_match_draw(p,q,0,out);
|
||||
}
|
||||
int duck_texture_pack_match_draw(DuckTexturePack* p, const HdTextureDrawQuery* q, int st, DuckTextureMatch* out) {
|
||||
if (out) *out = {};
|
||||
if (!p || !q || !out) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
Rect wanted{}; if (!query_rect(q, wanted)) return HD_TEXTURE_LOOKUP_NONE;
|
||||
try {
|
||||
for (const auto& c : p->match_cache) {
|
||||
const auto& a = c.query;
|
||||
if (c.st == (st != 0) && a.vram == q->vram && a.page_x == q->page_x && a.page_y == q->page_y &&
|
||||
a.depth == q->depth && a.clut_x == q->clut_x && a.clut_y == q->clut_y &&
|
||||
a.u_first == q->u_first && a.u_last == q->u_last && a.v_first == q->v_first && a.v_last == q->v_last) {
|
||||
*out = c.result; return c.status;
|
||||
}
|
||||
}
|
||||
const unsigned ppw = expansion(q->depth);
|
||||
size_t winner = size_t(-1); int origin_u = 0, origin_v = 0, rank = 2;
|
||||
bool ambiguous = false;
|
||||
const auto consider = [&](size_t index, int u, int v) {
|
||||
const Entry& e = p->entries[index]; const auto& k = e.key;
|
||||
const int candidate_rank = k.semitransparent == (st != 0) ? 0 : 1;
|
||||
if (candidate_rank > rank) return;
|
||||
bool valid = false;
|
||||
if (palette_hash(*q,k.palette_min,k.palette_max,valid) != k.palette_hash || !valid) return;
|
||||
if (candidate_rank < rank) { rank = candidate_rank; winner = size_t(-1); ambiguous = false; }
|
||||
if (e.ambiguous || winner != size_t(-1)) ambiguous = true;
|
||||
winner = index; origin_u = u; origin_v = v;
|
||||
};
|
||||
for (const auto& upload : p->uploads) {
|
||||
if (!contains(upload.rect,wanted)) continue;
|
||||
const auto found = p->uploads_by_hash[q->depth].find(upload.hash);
|
||||
if (found == p->uploads_by_hash[q->depth].end()) continue;
|
||||
for (const size_t index : found->second) {
|
||||
const auto& k = p->entries[index].key;
|
||||
if (upload.rect.w != k.source_width_words || upload.rect.h != k.source_height) continue;
|
||||
Rect rect{upload.rect.x+k.offset_x/ppw,upload.rect.y+k.offset_y,unsigned(k.width)/ppw,k.height};
|
||||
if (contains(rect,wanted)) consider(index,(int(upload.rect.x)-q->page_x)*int(ppw)+k.offset_x,
|
||||
int(upload.rect.y)-q->page_y+k.offset_y);
|
||||
}
|
||||
}
|
||||
size_t groups_checked = 0, hashed_bytes = 0; bool over_budget = false;
|
||||
for (const auto& item : p->page_groups[q->depth]) {
|
||||
const auto& group = item.second; const auto& r = group.texels;
|
||||
Rect rect{q->page_x+r.x/ppw,q->page_y+r.y,r.w/ppw,r.h};
|
||||
if (!contains(rect,wanted)) continue;
|
||||
const bool cached = std::any_of(p->rect_cache.begin(),p->rect_cache.end(),[&](const RectHash& c) {
|
||||
return c.vram == q->vram && c.rect.x == rect.x && c.rect.y == rect.y && c.rect.w == rect.w && c.rect.h == rect.h;
|
||||
});
|
||||
if (!cached) hashed_bytes += size_t(rect.w) * rect.h * 2;
|
||||
if (++groups_checked > 4096 || hashed_bytes > 4u*1024u*1024u) { over_budget = true; break; }
|
||||
const auto found = group.hashes.find(cached_hash(*p,*q,rect));
|
||||
if (found != group.hashes.end()) for (const size_t index : found->second) consider(index,r.x,r.y);
|
||||
}
|
||||
if (over_budget && !p->geometry_limit_reported) {
|
||||
note(*p,"page candidate geometry/hash budget exceeded");
|
||||
std::fprintf(stderr,"[texture-pack] page candidate geometry/hash budget exceeded; using native texture\n");
|
||||
p->geometry_limit_reported = true;
|
||||
}
|
||||
int status = over_budget ? HD_TEXTURE_LOOKUP_ERROR : winner == size_t(-1) ? HD_TEXTURE_LOOKUP_NONE :
|
||||
ambiguous ? HD_TEXTURE_LOOKUP_AMBIGUOUS : HD_TEXTURE_LOOKUP_FOUND;
|
||||
if (status == HD_TEXTURE_LOOKUP_FOUND) {
|
||||
const Entry& e = p->entries[winner]; out->key = e.key; out->entry_id = winner + 1;
|
||||
out->replacement_path = e.path.c_str(); out->origin_u = origin_u; out->origin_v = origin_v;
|
||||
out->source_width = e.key.width; out->source_height = e.key.height;
|
||||
}
|
||||
if (p->match_cache.size() >= 256) p->match_cache.erase(p->match_cache.begin());
|
||||
p->match_cache.push_back({*q,st != 0,status,*out});
|
||||
return status;
|
||||
} catch (...) { return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
void duck_texture_pack_set_decode_budget(DuckTexturePack* p, size_t bytes) {
|
||||
if (!p) return;
|
||||
std::lock_guard<std::mutex> lock(p->worker.mutex);
|
||||
p->worker.budget = bytes; p->worker.evict(0);
|
||||
}
|
||||
int duck_texture_pack_request_decode(DuckTexturePack* p, uint64_t id) {
|
||||
if (!p || !id || id > p->entries.size() || p->entries[id-1].ambiguous) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
try {
|
||||
Worker& w = p->worker; std::lock_guard<std::mutex> lock(w.mutex);
|
||||
auto it = w.decoded.find(id);
|
||||
if (it != w.decoded.end()) {
|
||||
it->second.use = ++w.tick;
|
||||
return it->second.status == DecodeStatus::Ready ? HD_TEXTURE_LOOKUP_FOUND :
|
||||
it->second.status == DecodeStatus::Failed ? HD_TEXTURE_LOOKUP_ERROR : HD_TEXTURE_LOOKUP_NONE;
|
||||
}
|
||||
if (!w.budget) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
if (w.decode_queue.size() >= kMaxDecodeQueue) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (w.decoded.size() >= 512) {
|
||||
auto victim = w.decoded.end();
|
||||
for (auto item = w.decoded.begin(); item != w.decoded.end(); ++item)
|
||||
if ((item->second.status == DecodeStatus::Ready || item->second.status == DecodeStatus::Failed) &&
|
||||
(victim == w.decoded.end() || item->second.use < victim->second.use)) victim = item;
|
||||
if (victim == w.decoded.end()) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (victim->second.image) w.used -= victim->second.image->rgba.size();
|
||||
w.decoded.erase(victim);
|
||||
}
|
||||
if (!w.thread.joinable()) w.thread = std::thread(worker_main,&w);
|
||||
w.decoded[id].path = p->entries[id-1].path; w.decode_queue.push_back(id);
|
||||
w.wake.notify_one(); return HD_TEXTURE_LOOKUP_NONE;
|
||||
} catch (...) { return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* p, uint64_t id, DuckTexturePixels* out) {
|
||||
if (out) *out = {};
|
||||
if (!p || !out) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
try {
|
||||
Worker& w = p->worker; std::lock_guard<std::mutex> lock(w.mutex); const auto it = w.decoded.find(id);
|
||||
if (it == w.decoded.end()) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (it->second.status != DecodeStatus::Ready) return it->second.status == DecodeStatus::Failed ? HD_TEXTURE_LOOKUP_ERROR : HD_TEXTURE_LOOKUP_NONE;
|
||||
auto* lease = new Lease{it->second.image}; it->second.use = ++w.tick;
|
||||
out->lease = lease; out->rgba = lease->image->rgba.data(); out->width = lease->image->w;
|
||||
out->height = lease->image->h; out->stride = out->width * 4; return HD_TEXTURE_LOOKUP_FOUND;
|
||||
} catch (...) { return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
void duck_texture_pixels_release(DuckTexturePixels* p) { if (p) { delete static_cast<Lease*>(p->lease); *p = {}; } }
|
||||
|
||||
int duck_texture_pack_dump_draw(DuckTexturePack* p, const HdTextureDrawQuery* q, int st, char* error, size_t capacity) {
|
||||
if (!p || !q) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
Rect rect{}; if (!query_rect(q,rect)) return HD_TEXTURE_LOOKUP_NONE;
|
||||
try {
|
||||
Worker& w = p->worker;
|
||||
{ std::lock_guard<std::mutex> lock(w.mutex);
|
||||
if (!w.dump_error.empty()) { error_text(error,capacity,w.dump_error); return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
if (w.dump_queue.size() >= kMaxDumpQueue) return HD_TEXTURE_LOOKUP_NONE; }
|
||||
const unsigned ppw = expansion(q->depth); const Upload* upload = nullptr;
|
||||
for (const auto& u : p->uploads) if (contains(u.rect,rect)) { if (upload) { upload = nullptr; break; } upload = &u; }
|
||||
DuckTextureKey k{}; k.kind = upload ? DUCK_TEXTURE_UPLOAD : DUCK_TEXTURE_PAGE;
|
||||
k.depth = q->depth; k.semitransparent = st != 0;
|
||||
k.source_hash = upload ? upload->hash : cached_hash(*p,*q,rect);
|
||||
k.source_width_words = upload ? upload->rect.w : 256 / ppw; k.source_height = upload ? upload->rect.h : 256;
|
||||
k.offset_x = (rect.x - (upload ? upload->rect.x : q->page_x)) * ppw;
|
||||
k.offset_y = rect.y - (upload ? upload->rect.y : q->page_y); k.width = rect.w * ppw; k.height = rect.h;
|
||||
k.palette_max = q->depth == 0 ? 15 : q->depth == 1 ? 255 : 0;
|
||||
bool palette_valid = false; k.palette_hash = palette_hash(*q,0,k.palette_max,palette_valid);
|
||||
if (!palette_valid) return HD_TEXTURE_LOOKUP_NONE;
|
||||
const std::string name = stem(k);
|
||||
if (name.empty()) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (p->dumped.count(name)) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (p->dumped.size() >= kMaxDumps) {
|
||||
if (!p->dump_limit_reported) {
|
||||
note(*p,"dump session limit (8192) reached; reset dump tracking to continue");
|
||||
std::fprintf(stderr,"[texture-pack] dump session limit (8192) reached; reset dump tracking to continue\n");
|
||||
p->dump_limit_reported = true;
|
||||
}
|
||||
error_text(error,capacity,"texture dump session limit (8192) reached");
|
||||
return HD_TEXTURE_LOOKUP_ERROR;
|
||||
}
|
||||
auto image = std::make_shared<Image>(); image->w = k.width; image->h = k.height;
|
||||
image->rgba.resize(size_t(image->w) * image->h * 4);
|
||||
for (unsigned y = 0; y < image->h; ++y) for (unsigned x = 0; x < image->w; ++x) {
|
||||
uint16_t color = q->vram[(rect.y+y)*1024 + rect.x + x/ppw];
|
||||
if (q->depth < 2) {
|
||||
const unsigned index = q->depth == 0 ? ((color >> ((x%4)*4)) & 15) : ((color >> ((x%2)*8)) & 255);
|
||||
color = q->clut_x + index < 1024 ? q->vram[q->clut_y*1024+q->clut_x+index] : 0;
|
||||
}
|
||||
uint8_t* pixel = image->rgba.data() + (size_t(y)*image->w+x)*4;
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const unsigned value = (color >> (c*5)) & 31;
|
||||
pixel[c] = static_cast<uint8_t>((value * 255u + 15u) / 31u);
|
||||
}
|
||||
pixel[3] = color == 0 ? 0 : (st && (color & 0x8000)) ? 128 : 255;
|
||||
}
|
||||
{ std::lock_guard<std::mutex> lock(w.mutex);
|
||||
if (w.dump_queue.size() >= kMaxDumpQueue) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (!w.thread.joinable()) w.thread = std::thread(worker_main,&w);
|
||||
w.dump_queue.push_back({p->dumps / (name + ".png"), std::move(image)});
|
||||
p->dumped.insert(name);
|
||||
w.wake.notify_one(); }
|
||||
return HD_TEXTURE_LOOKUP_FOUND;
|
||||
} catch (const std::exception& e) { error_text(error,capacity,e.what()); return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
catch (...) { error_text(error,capacity,"texture dump allocation failed"); return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
void duck_texture_pack_reset_dump(DuckTexturePack* p) {
|
||||
if (!p) return;
|
||||
p->dumped.clear();
|
||||
p->dump_limit_reported = false;
|
||||
std::lock_guard<std::mutex> lock(p->worker.mutex);
|
||||
p->worker.dump_error.clear();
|
||||
}
|
||||
} // extern C
|
||||
@@ -5712,6 +5712,7 @@ static void gp0_exec_cpu_to_vram(void) {
|
||||
/* 0 means max dimension */
|
||||
vram_write_w = (w == 0) ? 0x400 : (uint16_t)w;
|
||||
vram_write_h = (h == 0) ? 0x200 : (uint16_t)h;
|
||||
gr_vram_upload_begin(vram_write_x, vram_write_y, vram_write_w, vram_write_h);
|
||||
|
||||
/* A full history retains old uploads; later transfers must not append
|
||||
* to the last slot and overflow its diagnostic word counter. */
|
||||
|
||||
+266
-44
@@ -78,6 +78,7 @@
|
||||
#include "gl_scale_limits.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "mod_texture_banks.h"
|
||||
#include "frame_interpolation.h"
|
||||
#include "render_pass_plan.h"
|
||||
@@ -371,6 +372,8 @@ static int s_hr_scale = 1;
|
||||
#endif
|
||||
/* Netplay: SW@1× + GPU@s_out_scale dual write; CPU VRAM always authoritative. */
|
||||
static int s_cpu_auth_dual = 0;
|
||||
/* Independent of netplay: the replacement surface must never become VRAM. */
|
||||
static int s_hd_native_authority = 0;
|
||||
static int s_req_scale = 1; /* requested before context init */
|
||||
/* Driver limits queried at init: min(GL_MAX_TEXTURE_SIZE,
|
||||
* GL_MAX_RENDERBUFFER_SIZE, GL_MAX_VIEWPORT_DIMS); 0 before init. */
|
||||
@@ -567,7 +570,7 @@ static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0;
|
||||
* fragment shader read the noperspective varying — i.e. bit-identical to the
|
||||
* pre-feature pipeline. twin is the prim's GP0(E2h) texture window, its low 20
|
||||
* bits as a whole float (mask x, mask y, offset x, offset y; 5 bits each). */
|
||||
#define TEXV 21
|
||||
#define TEXV 26
|
||||
static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0;
|
||||
static GLuint s_blit_hi_prog = 0; /* windowed hi surface blit */
|
||||
static GLint s_uBhSrc = -1, s_uBhPass = -1, s_uBhMaskset = -1, s_uBhSrcDiv = -1;
|
||||
@@ -591,6 +594,7 @@ static float s_pq[3];
|
||||
|
||||
/* TEX program uniforms. */
|
||||
static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1;
|
||||
static GLint s_uHdTexture = -1;
|
||||
static GLint s_uPalette = -1;
|
||||
static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1;
|
||||
static GLint s_uMaskset = -1, s_uFilter = -1;
|
||||
@@ -1326,6 +1330,8 @@ static const char *TEX_VS =
|
||||
"layout(location=8) in float a_semi;\n"
|
||||
"layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n"
|
||||
"layout(location=10) in float a_twin; /* GP0(E2h) bits 0..19 */\n"
|
||||
"layout(location=11) in vec4 a_hd_source; /* page origin + native extent */\n"
|
||||
"layout(location=12) in float a_hd_mode;\n"
|
||||
"uniform float u_shift;\n"
|
||||
"uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n"
|
||||
"uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n"
|
||||
@@ -1337,12 +1343,14 @@ static const char *TEX_VS =
|
||||
"flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n"
|
||||
"flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n"
|
||||
"flat out int v_twin;\n"
|
||||
"flat out vec4 v_hd_source; flat out int v_hd_mode;\n"
|
||||
"void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\n"
|
||||
" v_persp = (a_q > 0.0) ? 1 : 0;\n"
|
||||
" v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n"
|
||||
" v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n"
|
||||
" v_semi = int(a_semi + 0.5);\n"
|
||||
" v_twin = int(a_twin + 0.5);\n"
|
||||
" v_hd_source=a_hd_source; v_hd_mode=int(a_hd_mode+0.5);\n"
|
||||
" v_limits = ivec4(floor(a_limits + 0.5));\n"
|
||||
" /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n"
|
||||
" * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n"
|
||||
@@ -1371,6 +1379,8 @@ static const char *TEX_FS =
|
||||
"flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n"
|
||||
"flat in int v_semi; /* GP0 command has semi-transparency enabled */\n"
|
||||
"flat in int v_twin; /* texture window, GP0(E2h) bits 0..19 */\n"
|
||||
"flat in vec4 v_hd_source; flat in int v_hd_mode;\n"
|
||||
"uniform sampler2D u_hd_texture;\n"
|
||||
"uniform usampler2D u_vram;\n"
|
||||
"uniform usampler2D u_palette;\n"
|
||||
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
|
||||
@@ -1427,6 +1437,17 @@ static const char *TEX_FS =
|
||||
" }\n"
|
||||
" return sum;\n"
|
||||
"}\n"
|
||||
"vec4 hd_texel(vec2 uv){\n"
|
||||
" ivec2 p=ivec2(floor(uv)); vec2 f=fract(uv);\n"
|
||||
" p &= ivec2(255);\n"
|
||||
" ivec4 tw=ivec4(v_twin&31,(v_twin>>5)&31,(v_twin>>10)&31,(v_twin>>15)&31);\n"
|
||||
" if((tw.x|tw.y)!=0) p=(p & ~(tw.xy*8)) | ((tw.zw & tw.xy)*8);\n"
|
||||
" else p=clamp(p,v_limits.xy,v_limits.zw);\n"
|
||||
" vec2 t=(vec2(p)+f-v_hd_source.xy)/v_hd_source.zw;\n"
|
||||
" ivec2 size=textureSize(u_hd_texture,0);\n"
|
||||
" ivec2 at=clamp(ivec2(floor(t*vec2(size))),ivec2(0),size-ivec2(1));\n"
|
||||
" return texelFetch(u_hd_texture,at,0);\n"
|
||||
"}\n"
|
||||
"void main(){\n"
|
||||
" int stp; vec3 rgb;\n"
|
||||
" /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n"
|
||||
@@ -1434,7 +1455,20 @@ static const char *TEX_FS =
|
||||
" * default is the PS1's affine (noperspective) mapping. */\n"
|
||||
" vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n"
|
||||
" vec2 dx=dFdx(uv), dy=dFdy(uv);\n"
|
||||
" if (u_filter == 0) {\n"
|
||||
" if (v_hd_mode != 0) {\n"
|
||||
" vec4 hd=hd_texel(uv);\n"
|
||||
" rgb=hd.rgb;\n"
|
||||
" if(v_hd_mode==1){\n"
|
||||
" if(hd.a==0.0) discard;\n"
|
||||
" int native=fetch_texel(int(floor(uv.x)),int(floor(uv.y)));\n"
|
||||
" if(native==0) discard; stp=(native>>15)&1;\n"
|
||||
" } else if(v_hd_mode==2){\n"
|
||||
" if(hd.a<128.0/255.0) discard; stp=0;\n"
|
||||
" } else {\n"
|
||||
" stp=hd.a<=242.0/255.0 ? 1 : 0;\n"
|
||||
" if(all(equal(hd,vec4(0.0))) || (stp==0 && all(equal(hd.rgb,vec3(0.0))))) discard;\n"
|
||||
" }\n"
|
||||
" } else if (u_filter == 0) {\n"
|
||||
" int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n"
|
||||
" if (raw == 0) discard;\n"
|
||||
" rgb = col5(raw);\n"
|
||||
@@ -2266,6 +2300,18 @@ static void pack_flush(void) {
|
||||
rect_clear(&s_pack_dirty);
|
||||
coh_record(GL_COH_PACK, x, y, x + w - 1, y + h - 1);
|
||||
|
||||
if (s_hd_native_authority) {
|
||||
/* All pending draws consumed the previous raw version before this
|
||||
* refresh. Never encode the HD presentation image into native words. */
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_raw_tex);
|
||||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, VRAM_W);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, PSXGL_RED_INTEGER,
|
||||
GL_UNSIGNED_SHORT, s_vram + (size_t)y * VRAM_W + x);
|
||||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo);
|
||||
glViewport(0, 0, VRAM_W, VRAM_H);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
@@ -2312,7 +2358,7 @@ static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth,
|
||||
static int s_depth24_skip_up = 0;
|
||||
static DirtyRect s_d24_skip_fb; /* union of skipped MDEC FB rects (VRAM halfwords) */
|
||||
static int cpu_vram_authoritative(void) {
|
||||
return s_cpu_auth_dual || s_depth24_skip_up;
|
||||
return s_cpu_auth_dual || s_hd_native_authority || s_depth24_skip_up;
|
||||
}
|
||||
|
||||
static void ensure_cpu(void) {
|
||||
@@ -2575,6 +2621,89 @@ static int s_tb_n = 0; /* verts queued */
|
||||
static int s_tb_semi = -2;
|
||||
static int s_tb_mask = 0, s_tb_filter = 0;
|
||||
static GLuint s_tb_bank_tex;
|
||||
static GLuint s_tb_hd_tex;
|
||||
|
||||
#define HD_GL_CACHE_CAP 64
|
||||
#define HD_GL_CACHE_BUDGET (128u * 1024u * 1024u)
|
||||
typedef struct HdGlTexture {
|
||||
uint64_t generation, key;
|
||||
uint64_t last_use;
|
||||
GLuint texture;
|
||||
size_t bytes;
|
||||
} HdGlTexture;
|
||||
static HdGlTexture s_hd_gl_cache[HD_GL_CACHE_CAP];
|
||||
static size_t s_hd_gl_cache_bytes;
|
||||
static uint64_t s_hd_gl_cache_clock;
|
||||
|
||||
void gl_renderer_clear_hd_texture_cache(void) {
|
||||
flush_flat_batch(); flush_tex_batch(); hiw_flush_queue();
|
||||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i) {
|
||||
if (s_ctx && s_hd_gl_cache[i].texture)
|
||||
glDeleteTextures(1, &s_hd_gl_cache[i].texture);
|
||||
}
|
||||
memset(s_hd_gl_cache, 0, sizeof(s_hd_gl_cache));
|
||||
s_hd_gl_cache_bytes = 0; s_tb_hd_tex = 0;
|
||||
}
|
||||
static GLuint hd_gl_texture(const GpuHdTextureImage* image) {
|
||||
if (!image->rgba || !image->width || !image->height || !image->source_width ||
|
||||
!image->source_height || image->stride != (uint64_t)image->width * 4u ||
|
||||
image->width > (uint32_t)s_gl_max_dim || image->height > (uint32_t)s_gl_max_dim)
|
||||
return 0;
|
||||
size_t bytes = (size_t)image->stride * image->height;
|
||||
if (bytes > HD_GL_CACHE_BUDGET) return 0;
|
||||
int free_slot = -1;
|
||||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i) {
|
||||
HdGlTexture* texture = &s_hd_gl_cache[i];
|
||||
if (texture->texture && texture->key == image->cache_key &&
|
||||
texture->generation == image->generation) {
|
||||
texture->last_use = ++s_hd_gl_cache_clock; return texture->texture;
|
||||
}
|
||||
if (!texture->texture && free_slot < 0) free_slot = i;
|
||||
}
|
||||
while (free_slot < 0 || s_hd_gl_cache_bytes + bytes > HD_GL_CACHE_BUDGET) {
|
||||
int oldest = -1;
|
||||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i)
|
||||
if (s_hd_gl_cache[i].texture && (oldest < 0 ||
|
||||
s_hd_gl_cache[i].last_use < s_hd_gl_cache[oldest].last_use)) oldest = i;
|
||||
if (oldest < 0) return 0;
|
||||
flush_flat_batch(); flush_tex_batch(); hiw_flush_queue();
|
||||
HdGlTexture* victim = &s_hd_gl_cache[oldest];
|
||||
glDeleteTextures(1, &victim->texture);
|
||||
s_hd_gl_cache_bytes -= victim->bytes;
|
||||
memset(victim, 0, sizeof(*victim)); free_slot = oldest;
|
||||
}
|
||||
/* A newly allocated image cannot affect pending draws, but TextureImage
|
||||
* changes GL bindings; their flush always binds its own complete state. */
|
||||
HdGlTexture* texture = &s_hd_gl_cache[free_slot];
|
||||
glGenTextures(1, &texture->texture);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 2);
|
||||
glBindTexture(GL_TEXTURE_2D, texture->texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image->width, image->height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, image->rgba);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
if (glGetError() != GL_NO_ERROR) {
|
||||
glDeleteTextures(1, &texture->texture); texture->texture = 0; return 0;
|
||||
}
|
||||
texture->key = image->cache_key; texture->generation = image->generation;
|
||||
texture->last_use = ++s_hd_gl_cache_clock;
|
||||
texture->bytes = bytes; s_hd_gl_cache_bytes += bytes;
|
||||
return texture->texture;
|
||||
}
|
||||
|
||||
static void bind_textured_resources(GLuint source, GLuint palette, GLuint hd) {
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, source); p_glUniform1i(s_uVram, 0);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 1);
|
||||
glBindTexture(GL_TEXTURE_2D, palette); p_glUniform1i(s_uPalette, 1);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 2);
|
||||
glBindTexture(GL_TEXTURE_2D, hd ? hd : s_up_tex); p_glUniform1i(s_uHdTexture, 2);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
}
|
||||
static int s_tb_twin[4] = {0, 0, 0, 0};
|
||||
static uint64_t s_batch_total = 0, s_batch_reason[7];
|
||||
|
||||
@@ -2729,6 +2858,7 @@ typedef struct {
|
||||
uint8_t check; /* mask-check state at draw time */
|
||||
uint8_t filter;
|
||||
GLuint tex; /* sampled texture (bank or raw mirror) */
|
||||
GLuint palette, hd_tex;
|
||||
int ax0, ay0, ax1, ay1; /* draw area, inclusive */
|
||||
size_t vfirst; /* float offset into s_hq_v */
|
||||
int vcount; /* vertices */
|
||||
@@ -2801,6 +2931,8 @@ static int hiw_enqueue_tex(int nverts, int semi, int mirror, int gate) {
|
||||
c->mask = (uint8_t)s_tb_mask;
|
||||
c->filter = (uint8_t)s_tb_filter;
|
||||
c->tex = s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex;
|
||||
c->palette = s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex;
|
||||
c->hd_tex = s_tb_hd_tex;
|
||||
if (wq) hiw_wide_set(c, gate);
|
||||
return wq;
|
||||
}
|
||||
@@ -2857,7 +2989,7 @@ static void hiw_replay_wide(void) {
|
||||
p_glUniform1f(s_tex_uXoff, (float)c->wdx);
|
||||
p_glUniform1f(s_tex_uXscale, c->wscale);
|
||||
p_glUniform1f(s_tex_uXcenter, c->wcenter);
|
||||
glBindTexture(GL_TEXTURE_2D, c->tex);
|
||||
bind_textured_resources(c->tex, c->palette, c->hd_tex);
|
||||
p_glUniform1i(s_uMaskset, c->mask);
|
||||
p_glUniform1i(s_uFilter, c->filter);
|
||||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)),
|
||||
@@ -2933,7 +3065,7 @@ static void hiw_flush_queue(void) {
|
||||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo);
|
||||
cur = HQ_TEX;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, c->tex);
|
||||
bind_textured_resources(c->tex, c->palette, c->hd_tex);
|
||||
p_glUniform1i(s_uMaskset, c->mask);
|
||||
p_glUniform1i(s_uFilter, c->filter);
|
||||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)),
|
||||
@@ -2978,13 +3110,8 @@ static void flush_tex_batch(void) {
|
||||
|
||||
hr_begin(1);
|
||||
p_glUseProgram(s_tex_prog);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex);
|
||||
p_glUniform1i(s_uVram, 0);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 1);
|
||||
glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex);
|
||||
p_glUniform1i(s_uPalette, 1);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
bind_textured_resources(s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex,
|
||||
s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex, s_tb_hd_tex);
|
||||
p_glUniform1i(s_uMaskset, s_tb_mask);
|
||||
p_glUniform1i(s_uFilter, s_tb_filter);
|
||||
p_glBindVertexArray(s_tex_vao);
|
||||
@@ -3386,6 +3513,18 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y);
|
||||
else if (s_selected_bank_live_clut)
|
||||
flush_pack_if_sampling(-1, 0, depth, clut_x, clut_y);
|
||||
GpuHdTextureImage hd = {0};
|
||||
GLuint hd_tex = 0;
|
||||
if (s_hd_native_authority && !s_selected_bank_tex) {
|
||||
uint32_t window = (uint32_t)(s_tw_mask_x | (s_tw_mask_y << 5) |
|
||||
(s_tw_off_x << 10) | (s_tw_off_y << 15));
|
||||
if (gpu_hd_textures_acquire_draw(texpage, clut_x, clut_y, lim, window,
|
||||
semi >= 0, &hd)) {
|
||||
hd_tex = hd_gl_texture(&hd);
|
||||
if (!hd_tex) hd.alpha_mode = 0;
|
||||
else gpu_hd_textures_note_applied();
|
||||
}
|
||||
}
|
||||
mark_prim_dirty(xs, ys, 3, 1 /* textured */);
|
||||
|
||||
/* Append to the textured batch. Flush first if this prim's blend/mask/twin/
|
||||
@@ -3426,7 +3565,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
!mod_texture_bank_batchable(s_selected_bank_tex != 0, s_mask_check, semi);
|
||||
int reason = -1;
|
||||
if (s_tb_n > 0) {
|
||||
if (s_tb_bank_tex != s_selected_bank_tex || s_tb_bank_live_clut != s_selected_bank_live_clut) reason = 0;
|
||||
if (s_tb_bank_tex != s_selected_bank_tex || s_tb_bank_live_clut != s_selected_bank_live_clut || s_tb_hd_tex != hd_tex) reason = 0;
|
||||
else if (isolate) reason = 0;
|
||||
else if (batch_semi != s_tb_semi) reason = 1;
|
||||
else if (s_mask_set != s_tb_mask) reason = 2;
|
||||
@@ -3445,6 +3584,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = filter; s_tb_gate = gate;
|
||||
s_tb_bank_tex = s_selected_bank_tex;
|
||||
s_tb_bank_live_clut = s_selected_bank_live_clut;
|
||||
s_tb_hd_tex = hd_tex;
|
||||
s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy;
|
||||
}
|
||||
/* The prim's window, exact as a float (20 bits). */
|
||||
@@ -3464,10 +3604,14 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */
|
||||
vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */
|
||||
vp[20] = twin; /* a_twin */
|
||||
vp[21] = (float)hd.origin_u; vp[22] = (float)hd.origin_v;
|
||||
vp[23] = (float)hd.source_width; vp[24] = (float)hd.source_height;
|
||||
vp[25] = (float)hd.alpha_mode;
|
||||
}
|
||||
s_tb_n += 3;
|
||||
if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */
|
||||
}
|
||||
gpu_hd_textures_release_image(&hd);
|
||||
}
|
||||
|
||||
/* Draw a flat-colored rect (GEO program) DIRECTLY into the active wide surface
|
||||
@@ -3780,8 +3924,17 @@ static int cpu_raster_required(void) {
|
||||
* GPU surface. The CPU mirror stays the authoritative record of the real
|
||||
* frame, so the pass never rasterizes into it. depth24 scanout still
|
||||
* presents from the CPU mirror and keeps its write-through. */
|
||||
return !s_raster_ok || s_depth24_skip_up || (s_cpu_auth_dual && !s_pass_active);
|
||||
return !s_raster_ok || s_depth24_skip_up ||
|
||||
((s_cpu_auth_dual || s_hd_native_authority) && !s_pass_active);
|
||||
}
|
||||
/* Bracket each authoritative draw so a later netplay/session policy change
|
||||
* cannot accidentally disable the faithful floor for HD replacement mode. */
|
||||
static int native_draw_begin(void) {
|
||||
int previous = sw_faithful_authority();
|
||||
if (s_hd_native_authority) sw_set_faithful_authority(1);
|
||||
return previous;
|
||||
}
|
||||
static void native_draw_end(int previous) { sw_set_faithful_authority(previous); }
|
||||
/* The sub-pixel / perspective override describes exactly one triangle; drop it
|
||||
* once that triangle has been submitted so a later prim can never inherit it. */
|
||||
static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; s_projected_uv_valid = 0; }
|
||||
@@ -3824,80 +3977,127 @@ void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3],
|
||||
}
|
||||
|
||||
static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) {
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1);
|
||||
precise_consumed();
|
||||
}
|
||||
static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) {
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1);
|
||||
precise_consumed();
|
||||
}
|
||||
static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){
|
||||
if (pass_refuse_write("fill", x, y, w, h)) return;
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_fill_rect(x,y,w,h,c);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_fill(x,y,w,h,c);
|
||||
}
|
||||
static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){
|
||||
if (pass_refuse_write("copy", dx, dy, w, h)) return;
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_copy_rect(sx,sy,dx,dy,w,h);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_copy_rect(sx,sy,dx,dy,w,h);
|
||||
}
|
||||
static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){
|
||||
if (cpu_raster_required())
|
||||
const int native_required = cpu_raster_required();
|
||||
if (native_required && !s_hd_native_authority)
|
||||
sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp);
|
||||
if (!s_raster_ok) return;
|
||||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||||
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
|
||||
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
|
||||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL);
|
||||
if (s_raster_ok) {
|
||||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||||
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
|
||||
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
|
||||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL);
|
||||
}
|
||||
if (native_required && s_hd_native_authority) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
precise_consumed();
|
||||
}
|
||||
static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){
|
||||
if (cpu_raster_required())
|
||||
const int native_required = cpu_raster_required();
|
||||
if (native_required && !s_hd_native_authority)
|
||||
sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw);
|
||||
if (!s_raster_ok) return;
|
||||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||||
uint32_t cc[3]={c0,c1,c2}; float col[9];
|
||||
for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; }
|
||||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL);
|
||||
if (s_raster_ok) {
|
||||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||||
uint32_t cc[3]={c0,c1,c2}; float col[9];
|
||||
for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; }
|
||||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL);
|
||||
}
|
||||
if (native_required && s_hd_native_authority) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
precise_consumed();
|
||||
}
|
||||
static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_flat_rect(x,y,w,h,c);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1);
|
||||
}
|
||||
static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){
|
||||
if (cpu_raster_required())
|
||||
const int native_required = cpu_raster_required();
|
||||
if (native_required && !s_hd_native_authority)
|
||||
sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp);
|
||||
if (!s_raster_ok) return;
|
||||
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||||
if (s_raster_ok)
|
||||
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||||
if (native_required && s_hd_native_authority) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
}
|
||||
static void glb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp){
|
||||
if (cpu_raster_required())
|
||||
const int native_required = cpu_raster_required();
|
||||
if (native_required && !s_hd_native_authority)
|
||||
sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp);
|
||||
if (!s_raster_ok) return;
|
||||
gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||||
if (s_raster_ok)
|
||||
gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||||
if (native_required && s_hd_native_authority) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
}
|
||||
static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_line(x0,y0,x1,y1,c);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1);
|
||||
}
|
||||
static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_shaded_line(x0,y0,c0,x1,y1,c1);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1);
|
||||
}
|
||||
@@ -4010,7 +4210,7 @@ static void depth24_clear_skipped_fb(void) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
rect_add(&s_pack_dirty, x0, y0, x1, y1);
|
||||
if (!s_cpu_auth_dual) {
|
||||
if (!cpu_vram_authoritative()) {
|
||||
rect_add(&s_cpu_dirty, x0, y0, x1, y1);
|
||||
s_gpu_dirty = 1; /* The clear must reach an immediate CPU read too. */
|
||||
}
|
||||
@@ -4446,6 +4646,7 @@ static int init_gpu_raster(void) {
|
||||
s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset");
|
||||
s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter");
|
||||
s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits");
|
||||
s_uHdTexture = p_glGetUniformLocation(s_tex_prog, "u_hd_texture");
|
||||
s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src");
|
||||
s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass");
|
||||
s_uBlitMaskset = p_glGetUniformLocation(s_blit_prog, "u_maskset");
|
||||
@@ -4536,6 +4737,8 @@ static int init_gpu_raster(void) {
|
||||
p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */
|
||||
p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */
|
||||
p_glVertexAttribPointer(10, 1, GL_FLOAT, GL_FALSE, st, (void*)(20*sizeof(float))); p_glEnableVertexAttribArray(10); /* twin */
|
||||
p_glVertexAttribPointer(11, 4, GL_FLOAT, GL_FALSE, st, (void*)(21*sizeof(float))); p_glEnableVertexAttribArray(11); /* HD source */
|
||||
p_glVertexAttribPointer(12, 1, GL_FLOAT, GL_FALSE, st, (void*)(25*sizeof(float))); p_glEnableVertexAttribArray(12); /* HD alpha mode */
|
||||
}
|
||||
|
||||
p_glGenVertexArrays(1, &s_blit_vao);
|
||||
@@ -4589,7 +4792,7 @@ static int init_gpu_raster(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual; }
|
||||
int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual && !s_hd_native_authority; }
|
||||
|
||||
int gl_renderer_fit_wide_aspect(int disp_w, int *num, int *den) {
|
||||
if (!s_raster_ok || s_gl_max_dim <= 0) return 0;
|
||||
@@ -4851,6 +5054,7 @@ static void pass_resources_release(void);
|
||||
|
||||
void gl_renderer_shutdown(void) {
|
||||
s_native_surface_enabled=s_native_surface_pending=0;
|
||||
gl_renderer_clear_hd_texture_cache();
|
||||
pass_resources_release();
|
||||
if (s_ctx) {
|
||||
for (unsigned i = 1; i < 65536u; ++i)
|
||||
@@ -5018,6 +5222,7 @@ void gl_renderer_invalidate_present(void) {
|
||||
}
|
||||
|
||||
void gl_renderer_restage_vram_after_savestate(void) {
|
||||
gpu_hd_textures_reset_tracking();
|
||||
if (!s_raster_ok || !s_vram) return;
|
||||
/* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO
|
||||
* even if a depth24 skip swallowed the restore, then re-arm scanout-band
|
||||
@@ -5032,7 +5237,7 @@ void gl_renderer_restage_vram_after_savestate(void) {
|
||||
depth24_mark_scanout_band();
|
||||
}
|
||||
/* Dual-raster: CPU is authority after snap; FBO is cosmetics only. */
|
||||
if (s_cpu_auth_dual) {
|
||||
if (s_cpu_auth_dual || s_hd_native_authority) {
|
||||
s_gpu_dirty = 0;
|
||||
rect_clear(&s_cpu_dirty);
|
||||
}
|
||||
@@ -5046,6 +5251,23 @@ void gl_renderer_set_cpu_auth_dual(int on) {
|
||||
}
|
||||
}
|
||||
|
||||
void gl_renderer_set_hd_texture_mode(int on) {
|
||||
on = on ? 1 : 0;
|
||||
if (on == s_hd_native_authority) return;
|
||||
if (s_raster_ok) {
|
||||
flush_flat_batch(); flush_tex_batch(); flush_cpu_upload(); hiw_flush_queue();
|
||||
if (on) ensure_cpu();
|
||||
else {
|
||||
/* Return GPU authority only after replacing the presentation
|
||||
* image with native VRAM; an HD frame must never be read back. */
|
||||
gl_renderer_restage_vram_after_savestate();
|
||||
}
|
||||
}
|
||||
s_hd_native_authority = on;
|
||||
s_gpu_dirty = 0; rect_clear(&s_cpu_dirty);
|
||||
if (on) s_selected_bank_tex = 0;
|
||||
}
|
||||
|
||||
int gl_renderer_cpu_auth_dual(void) {
|
||||
return s_cpu_auth_dual;
|
||||
}
|
||||
@@ -6172,7 +6394,7 @@ uint32_t gl_renderer_pass_unavailable(void) {
|
||||
s_interp_source != 1 || !(s_interp_source_hz > 0.0))
|
||||
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
|
||||
/* Dual raster (netplay CPU-authoritative VRAM) or a debug refusal. */
|
||||
if ((s_cpu_auth_dual && !render_pass_netplay_enabled()) || s_pass_force_refuse)
|
||||
if (s_hd_native_authority || (s_cpu_auth_dual && !render_pass_netplay_enabled()) || s_pass_force_refuse)
|
||||
return PSX_MOD_RENDER_PASS_BACKEND;
|
||||
/* Windowed high-resolution mode: the presented surfaces are the window
|
||||
* tiles at s_out_scale, which a pass (backing up s_hr_fbo at
|
||||
@@ -6711,7 +6933,7 @@ uint32_t gl_renderer_stereo_unavailable(void) {
|
||||
pass_refusal_init();
|
||||
if (!s_ctx || !s_raster_ok || !s_hr_fbo || gpu_display_is_depth24())
|
||||
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
|
||||
if (s_cpu_auth_dual || s_hiw || g_wide_w > 0 || s_pass_force_refuse > 0)
|
||||
if (s_cpu_auth_dual || s_hd_native_authority || s_hiw || g_wide_w > 0 || s_pass_force_refuse > 0)
|
||||
return PSX_MOD_RENDER_PASS_BACKEND;
|
||||
return PSX_MOD_RENDER_PASS_READY;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "hd_texture_pack.h"
|
||||
#include "duckstation_texture_pack.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
constexpr size_t kVramWords = 1024u * 512u;
|
||||
struct Session {
|
||||
std::string root;
|
||||
HdTexturePack* beetle = nullptr;
|
||||
DuckTexturePack* duck = nullptr;
|
||||
bool replacements = false;
|
||||
bool dump = false;
|
||||
bool dump_failed = false;
|
||||
GpuHdTextureDiag diag{};
|
||||
~Session() { hd_texture_pack_destroy(beetle); duck_texture_pack_destroy(duck); }
|
||||
};
|
||||
struct Lease {
|
||||
HdTexturePixels beetle{};
|
||||
DuckTexturePixels duck{};
|
||||
~Lease() { hd_texture_pixels_release(&beetle); duck_texture_pixels_release(&duck); }
|
||||
};
|
||||
std::unique_ptr<Session> session;
|
||||
const uint16_t* native_vram = nullptr;
|
||||
uint64_t generation = 1;
|
||||
|
||||
void fail(char* error, size_t capacity, const char* message) {
|
||||
if (error && capacity) std::snprintf(error, capacity, "%s", message);
|
||||
}
|
||||
bool is_beetle_root(const std::filesystem::path& root) {
|
||||
std::error_code error;
|
||||
if (std::filesystem::exists(root / "Hashes.ini", error)) return true;
|
||||
if (std::filesystem::exists(root.parent_path() / "Hashes.ini", error)) return true;
|
||||
const auto name = root.filename().u8string();
|
||||
const std::string suffix = "-texture-replacements";
|
||||
return name.size() >= suffix.size() &&
|
||||
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||||
}
|
||||
int window_coord(int value, int mask, int offset) {
|
||||
return (value & ~(mask * 8)) | ((offset & mask) * 8);
|
||||
}
|
||||
bool make_query(uint16_t texpage, uint16_t cx, uint16_t cy, const int limits[4],
|
||||
uint32_t window, HdTextureDrawQuery& query) {
|
||||
if (!native_vram || !limits || ((texpage >> 7) & 3u) == 3u) return false;
|
||||
if (limits[0] < 0 || limits[1] < 0 || limits[2] > 255 || limits[3] > 255 ||
|
||||
limits[0] > limits[2] || limits[1] > limits[3]) return false;
|
||||
int first[2] = {255,255}, last[2] = {0,0};
|
||||
for (int axis = 0; axis < 2; ++axis) {
|
||||
int mask = (window >> (axis * 5)) & 31;
|
||||
int offset = (window >> (10 + axis * 5)) & 31;
|
||||
for (int p = limits[axis]; p <= limits[axis + 2]; ++p) {
|
||||
const int mapped = window_coord(p, mask, offset);
|
||||
first[axis] = std::min(first[axis], mapped);
|
||||
last[axis] = std::max(last[axis], mapped);
|
||||
}
|
||||
}
|
||||
query = {};
|
||||
query.page_x = (texpage & 15u) * 64u;
|
||||
query.page_y = ((texpage >> 4) & 1u) * 256u;
|
||||
query.depth = (texpage >> 7) & 3u;
|
||||
query.u_first = static_cast<uint8_t>(first[0]);
|
||||
query.u_last = static_cast<uint8_t>(last[0]);
|
||||
query.v_first = static_cast<uint8_t>(first[1]);
|
||||
query.v_last = static_cast<uint8_t>(last[1]);
|
||||
query.clut_x = cx; query.clut_y = cy;
|
||||
query.vram = native_vram; query.vram_word_count = kVramWords;
|
||||
return true;
|
||||
}
|
||||
void dump_query(const HdTextureDrawQuery& query, int semi) {
|
||||
if (!session->dump || session->dump_failed) return;
|
||||
char error[256]{};
|
||||
const int status = duck_texture_pack_dump_draw(session->duck, &query, semi, error, sizeof(error));
|
||||
if (status == HD_TEXTURE_LOOKUP_FOUND) ++session->diag.dumped_textures;
|
||||
if (status < 0) {
|
||||
std::fprintf(stderr, "psxrecomp: texture dump stopped: %s\n", error);
|
||||
/* Keep the configured raster authority until a safe session boundary.
|
||||
* Tearing it down inside a pre-draw query would lose command order. */
|
||||
session->dump_failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int gpu_hd_textures_configure(const char* root, int replacements,
|
||||
int dump, char* error, size_t capacity) {
|
||||
if (error && capacity) error[0] = 0;
|
||||
if ((!root || !root[0]) && !replacements && !dump) {
|
||||
gpu_hd_textures_shutdown(); return 1;
|
||||
}
|
||||
if (!root || !root[0]) { fail(error, capacity, "Select an external texture-pack directory."); return 0; }
|
||||
try {
|
||||
auto next = std::make_unique<Session>();
|
||||
next->root = root; next->replacements = replacements != 0; next->dump = dump != 0;
|
||||
const bool beetle = is_beetle_root(std::filesystem::u8path(root));
|
||||
if (beetle) {
|
||||
if (!hd_texture_pack_create(root, &next->beetle, error, capacity)) return 0;
|
||||
} else {
|
||||
char ignored[256]{};
|
||||
hd_texture_pack_create(root, &next->beetle, ignored, sizeof(ignored));
|
||||
}
|
||||
if (!next->beetle) {
|
||||
auto directory = std::filesystem::u8path(root);
|
||||
if (directory.filename().u8string() == "replacements") directory = directory.parent_path();
|
||||
std::filesystem::create_directories(directory / "replacements");
|
||||
std::filesystem::create_directories(directory / "dumps");
|
||||
}
|
||||
if (!duck_texture_pack_create(root, &next->duck, error, capacity)) return 0;
|
||||
if (session && session->root == next->root) {
|
||||
if (!duck_texture_pack_copy_tracking(next->duck, session->duck)) {
|
||||
fail(error, capacity, "Could not preserve texture-upload tracking during reload."); return 0;
|
||||
}
|
||||
if (session->beetle && next->beetle) {
|
||||
uint8_t* saved = nullptr; size_t saved_size = 0;
|
||||
if (!hd_texture_pack_tracking_state_save(session->beetle, &saved, &saved_size)) {
|
||||
fail(error, capacity, "Could not save Beetle texture-upload tracking during reload."); return 0;
|
||||
}
|
||||
const int restored = hd_texture_pack_tracking_state_load(next->beetle, saved, saved_size);
|
||||
std::free(saved);
|
||||
if (!restored) { fail(error, capacity, "Could not restore Beetle texture-upload tracking during reload."); return 0; }
|
||||
}
|
||||
}
|
||||
/* This also flushes all queued users before old replacement textures
|
||||
* and pixel leases may be destroyed. */
|
||||
gl_renderer_set_hd_texture_mode(next->replacements || next->dump);
|
||||
gl_renderer_clear_hd_texture_cache();
|
||||
session = std::move(next); ++generation;
|
||||
DuckTexturePackInfo info{};
|
||||
duck_texture_pack_get_info(session->duck, &info);
|
||||
size_t replacements_found = info.replacement_count;
|
||||
size_t ambiguous = info.ambiguous_count;
|
||||
if (session->beetle) {
|
||||
HdTexturePackInfo beetle_info{};
|
||||
hd_texture_pack_get_info(session->beetle, &beetle_info);
|
||||
replacements_found = beetle_info.unique_key_count;
|
||||
ambiguous = beetle_info.ambiguous_key_count;
|
||||
}
|
||||
std::fprintf(stdout, "psxrecomp: texture pack '%s': %zu replacements, %zu ambiguous, %zu ignored\n",
|
||||
session->root.c_str(), replacements_found, ambiguous, info.ignored_count);
|
||||
if (info.diagnostic && info.diagnostic[0])
|
||||
std::fprintf(stderr, "psxrecomp: texture pack: %s\n", info.diagnostic);
|
||||
return 1;
|
||||
} catch (const std::exception& exception) {
|
||||
fail(error, capacity, exception.what()); return 0;
|
||||
}
|
||||
}
|
||||
extern "C" void gpu_hd_textures_shutdown(void) {
|
||||
gl_renderer_set_hd_texture_mode(0);
|
||||
gl_renderer_clear_hd_texture_cache();
|
||||
session.reset(); ++generation;
|
||||
}
|
||||
extern "C" int gpu_hd_textures_reload(char* error, size_t capacity) {
|
||||
if (!session) { fail(error, capacity, "No texture-pack session is active."); return 0; }
|
||||
const std::string root = session->root;
|
||||
return gpu_hd_textures_configure(root.c_str(), session->replacements, session->dump, error, capacity);
|
||||
}
|
||||
extern "C" void gpu_hd_textures_set_dump_enabled(int enabled) {
|
||||
if (!session || session->dump == (enabled != 0)) return;
|
||||
gl_renderer_set_hd_texture_mode(session->replacements || enabled);
|
||||
session->dump = enabled != 0;
|
||||
session->dump_failed = false;
|
||||
}
|
||||
extern "C" int gpu_hd_textures_replacements_enabled(void) { return session && session->replacements; }
|
||||
extern "C" int gpu_hd_textures_dump_enabled(void) { return session && session->dump; }
|
||||
extern "C" int gpu_hd_textures_active(void) { return session && (session->replacements || session->dump); }
|
||||
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) {
|
||||
if (!out) return;
|
||||
*out = {};
|
||||
if (!session) { out->root = ""; return; }
|
||||
*out = session->diag;
|
||||
out->root = session->root.c_str(); out->active = gpu_hd_textures_active();
|
||||
out->replacements = session->replacements; out->dump = session->dump && !session->dump_failed;
|
||||
out->format = session->beetle ? 1 : 2;
|
||||
if (session->beetle) { HdTexturePackInfo info{}; hd_texture_pack_get_info(session->beetle, &info); out->replacement_count = info.unique_key_count; }
|
||||
else { DuckTexturePackInfo info{}; duck_texture_pack_get_info(session->duck, &info); out->replacement_count = info.replacement_count; }
|
||||
}
|
||||
extern "C" void gpu_hd_textures_note_applied(void) { if (session) ++session->diag.applied_draws; }
|
||||
extern "C" void gpu_hd_textures_set_vram(const uint16_t* vram) { native_vram = vram; gpu_hd_textures_reset_tracking(); }
|
||||
extern "C" void gpu_hd_textures_reset_tracking(void) {
|
||||
if (!session) return;
|
||||
hd_texture_pack_reset_tracking(session->beetle);
|
||||
duck_texture_pack_reset_tracking(session->duck);
|
||||
}
|
||||
extern "C" void gpu_hd_textures_invalidate(int x, int y, int width, int height) {
|
||||
if (!session || width < 1 || height < 1) return;
|
||||
if (width > 1024 || height > 512) { gpu_hd_textures_reset_tracking(); return; }
|
||||
hd_texture_pack_invalidate(session->beetle, x & 1023, y & 511, width, height);
|
||||
duck_texture_pack_invalidate(session->duck, x & 1023, y & 511, width, height);
|
||||
}
|
||||
extern "C" void gpu_hd_textures_track_upload(int x, int y, int width, int height,
|
||||
const uint16_t* words) {
|
||||
if (!session || !native_vram || !words || width < 1 || width > 1024 || height < 1 || height > 512) return;
|
||||
if (width == 1024 && height == 512) { gpu_hd_textures_reset_tracking(); return; }
|
||||
/* gpu.c's A0 staging already contains post-mask words. Read the canonical
|
||||
* mirror here also for host-initiated transfers which use raw input. */
|
||||
const size_t count = static_cast<size_t>(width) * height;
|
||||
std::unique_ptr<uint16_t[]> applied(new (std::nothrow) uint16_t[count]);
|
||||
if (!applied) { gpu_hd_textures_reset_tracking(); return; }
|
||||
for (int row = 0; row < height; ++row)
|
||||
for (int col = 0; col < width; ++col)
|
||||
applied[static_cast<size_t>(row) * width + col] =
|
||||
native_vram[((y + row) & 511) * 1024 + ((x + col) & 1023)];
|
||||
hd_texture_pack_track_upload(session->beetle, x & 1023, y & 511, width, height,
|
||||
applied.get(), count, nullptr);
|
||||
duck_texture_pack_track_upload(session->duck, x & 1023, y & 511, width, height,
|
||||
native_vram, kVramWords);
|
||||
}
|
||||
extern "C" int gpu_hd_textures_acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy,
|
||||
const int limits[4], uint32_t window,
|
||||
int semi, GpuHdTextureImage* image) {
|
||||
if (!image) return 0;
|
||||
*image = {};
|
||||
if (!session) return 0;
|
||||
++session->diag.draw_queries;
|
||||
HdTextureDrawQuery query{};
|
||||
if (!make_query(tp, cx, cy, limits, window, query)) return 0;
|
||||
dump_query(query, semi);
|
||||
if (!session->replacements) return 0;
|
||||
auto lease = std::unique_ptr<Lease>(new (std::nothrow) Lease);
|
||||
if (!lease) return 0;
|
||||
if (session->beetle) {
|
||||
HdTextureMatch match{};
|
||||
if (hd_texture_pack_match(session->beetle, &query, &match) != HD_TEXTURE_LOOKUP_FOUND) return 0;
|
||||
++session->diag.matched_draws;
|
||||
hd_texture_pack_request_decode(session->beetle, match.entry.texture_hash, match.entry.palette_hash);
|
||||
if (hd_texture_pack_acquire_decoded(session->beetle, match.entry.texture_hash,
|
||||
match.entry.palette_hash, &lease->beetle) != HD_TEXTURE_LOOKUP_FOUND) return 0;
|
||||
const unsigned pixels_per_word = 4u >> query.depth;
|
||||
image->rgba = lease->beetle.rgba; image->width = lease->beetle.width;
|
||||
image->height = lease->beetle.height; image->stride = lease->beetle.stride;
|
||||
image->source_width = match.upload_width_words * pixels_per_word;
|
||||
image->source_height = match.upload_height;
|
||||
image->origin_u = query.u_first - (match.source_word_x * pixels_per_word + (query.u_first % pixels_per_word));
|
||||
image->origin_v = query.v_first - match.source_y;
|
||||
image->cache_key = (static_cast<uint64_t>(match.entry.texture_hash) << 32) | match.entry.palette_hash;
|
||||
image->alpha_mode = 1;
|
||||
} else {
|
||||
DuckTextureMatch match{};
|
||||
if (duck_texture_pack_match_draw(session->duck, &query, semi, &match) != HD_TEXTURE_LOOKUP_FOUND) return 0;
|
||||
++session->diag.matched_draws;
|
||||
duck_texture_pack_request_decode(session->duck, match.entry_id);
|
||||
if (duck_texture_pack_acquire_decoded(session->duck, match.entry_id, &lease->duck) != HD_TEXTURE_LOOKUP_FOUND) return 0;
|
||||
image->rgba = lease->duck.rgba; image->width = lease->duck.width;
|
||||
image->height = lease->duck.height; image->stride = lease->duck.stride;
|
||||
image->source_width = match.source_width; image->source_height = match.source_height;
|
||||
image->origin_u = match.origin_u; image->origin_v = match.origin_v;
|
||||
image->cache_key = match.entry_id;
|
||||
image->alpha_mode = match.key.semitransparent ? 3 : 2;
|
||||
}
|
||||
image->generation = generation; image->lease = lease.release();
|
||||
++session->diag.ready_draws;
|
||||
return 1;
|
||||
}
|
||||
extern "C" void gpu_hd_textures_release_image(GpuHdTextureImage* image) {
|
||||
if (!image) return;
|
||||
delete static_cast<Lease*>(image->lease); *image = {};
|
||||
}
|
||||
extern "C" void gpu_hd_textures_observe_draw(uint16_t tp, uint16_t cx, uint16_t cy,
|
||||
const int limits[4], uint32_t window, int semi) {
|
||||
if (!session || !session->dump || session->dump_failed) return;
|
||||
HdTextureDrawQuery query{};
|
||||
if (make_query(tp, cx, cy, limits, window, query)) {
|
||||
++session->diag.draw_queries; dump_query(query, semi);
|
||||
}
|
||||
}
|
||||
+70
-10
@@ -12,6 +12,8 @@
|
||||
|
||||
#include "gpu_render.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "gpu_uv.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static const GpuRenderBackend SW_BACKEND = {
|
||||
@@ -65,6 +67,47 @@ extern const GpuRenderBackend *vk_backend_get(void);
|
||||
|
||||
static const GpuRenderBackend *g_b = &SW_BACKEND;
|
||||
static GrBackend g_effective = GR_BACKEND_SOFTWARE;
|
||||
static int g_hd_offset_x, g_hd_offset_y;
|
||||
static uint32_t g_hd_texture_window;
|
||||
static int g_hd_semi, g_hd_perspective;
|
||||
static void hd_observe_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,
|
||||
int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp) {
|
||||
if (g_effective == GR_BACKEND_OPENGL || !gpu_hd_textures_dump_enabled()) return;
|
||||
if (g_effective == GR_BACKEND_VULKAN) g_b->vram_read(0,0);
|
||||
int limits[4], xs[3]={x0,x1,x2},ys[3]={y0,y1,y2},us[3]={u0,u1,u2},vs[3]={v0,v1,v2};
|
||||
if (g_hd_perspective) psx_uv_tri_world_limits(us,vs,limits);
|
||||
else psx_uv_tri_limits(xs,ys,us,vs,limits);
|
||||
gpu_hd_textures_observe_draw(tp,cx,cy,limits,g_hd_texture_window,g_hd_semi);
|
||||
}
|
||||
static void hd_observe_rect(int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp) {
|
||||
if (g_effective == GR_BACKEND_OPENGL || !gpu_hd_textures_dump_enabled()) return;
|
||||
if (g_effective == GR_BACKEND_VULKAN) g_b->vram_read(0,0);
|
||||
int limits[4]; psx_uv_rect_limits(u0,v0,u1,v1,limits);
|
||||
gpu_hd_textures_observe_draw(tp,cx,cy,limits,g_hd_texture_window,g_hd_semi);
|
||||
}
|
||||
|
||||
/* Residency describes guest native words, so any draw/copy which can touch
|
||||
* an upload ends its identity. Run after submission: an overlapping texture
|
||||
* draw must resolve its source before the destination invalidates it. */
|
||||
static void hd_invalidate_draw(int x0, int y0, int x1, int y1) {
|
||||
int ax0, ay0, ax1, ay1;
|
||||
if (!gpu_hd_textures_active()) return;
|
||||
g_b->get_draw_area(&ax0, &ay0, &ax1, &ay1);
|
||||
x0 += g_hd_offset_x; x1 += g_hd_offset_x;
|
||||
y0 += g_hd_offset_y; y1 += g_hd_offset_y;
|
||||
if (x0 < ax0) x0 = ax0; if (x1 > ax1) x1 = ax1;
|
||||
if (y0 < ay0) y0 = ay0; if (y1 > ay1) y1 = ay1;
|
||||
if (x0 <= x1 && y0 <= y1)
|
||||
gpu_hd_textures_invalidate(x0, y0, x1 - x0 + 1, y1 - y0 + 1);
|
||||
}
|
||||
static void hd_invalidate_triangle(int x0, int y0, int x1, int y1, int x2, int y2) {
|
||||
int xmin = x0, xmax = x0, ymin = y0, ymax = y0;
|
||||
if (x1 < xmin) xmin = x1; if (x2 < xmin) xmin = x2;
|
||||
if (x1 > xmax) xmax = x1; if (x2 > xmax) xmax = x2;
|
||||
if (y1 < ymin) ymin = y1; if (y2 < ymin) ymin = y2;
|
||||
if (y1 > ymax) ymax = y1; if (y2 > ymax) ymax = y2;
|
||||
hd_invalidate_draw(xmin, ymin, xmax, ymax);
|
||||
}
|
||||
|
||||
void gr_set_backend(GrBackend backend) {
|
||||
if (backend == GR_BACKEND_OPENGL) {
|
||||
@@ -95,14 +138,14 @@ void gr_set_backend(GrBackend backend) {
|
||||
GrBackend gr_backend(void) { return g_effective; }
|
||||
|
||||
/* ---- Dispatch wrappers (one line each; forward to the active backend) ---- */
|
||||
void gr_init(uint16_t *vram) { g_b->init(vram); }
|
||||
void gr_init(uint16_t *vram) { g_b->init(vram); gpu_hd_textures_set_vram(vram); g_hd_offset_x = g_hd_offset_y = 0; g_hd_texture_window = 0; g_hd_semi = g_hd_perspective = 0; }
|
||||
void gr_set_scale(int scale) { g_b->set_scale(scale); }
|
||||
int gr_scale(void) { return g_b->scale(); }
|
||||
void gr_set_texture_filter(int bilinear) { g_b->set_texture_filter(bilinear); }
|
||||
int gr_texture_filter(void) { return g_b->texture_filter(); }
|
||||
void gr_set_semi_transparency(int e, int m) { g_b->set_semi_transparency(e, m); }
|
||||
void gr_set_semi_transparency(int e, int m) { g_b->set_semi_transparency(e, m); g_hd_semi=e; }
|
||||
void gr_set_mask_bits(int s, int c) { g_b->set_mask_bits(s, c); }
|
||||
void gr_set_texture_window(uint32_t raw) { g_b->set_texture_window(raw); }
|
||||
void gr_set_texture_window(uint32_t raw) { g_b->set_texture_window(raw); g_hd_texture_window=raw; }
|
||||
void gr_set_color_modulation(int r, int g, int b, int raw) { g_b->set_color_modulation(r, g, b, raw); }
|
||||
void gr_set_precise_triangle(int enabled, int32_t x0, int32_t y0, int32_t x1, int32_t y1,
|
||||
int32_t x2, int32_t y2) {
|
||||
@@ -110,44 +153,60 @@ void gr_set_precise_triangle(int enabled, int32_t x0, int32_t y0, int32_t x1, in
|
||||
g_b->set_precise_triangle(enabled, x0, y0, x1, y1, x2, y2);
|
||||
}
|
||||
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
|
||||
g_hd_perspective = enabled && q0 > 0 && q1 > 0 && q2 > 0;
|
||||
if (g_b->set_perspective_triangle)
|
||||
g_b->set_perspective_triangle(enabled, q0, q1, q2);
|
||||
}
|
||||
void gr_fill_rect(int x, int y, int w, int h, uint16_t c) { g_b->fill_rect(x, y, w, h, c); }
|
||||
void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h) { g_b->copy_rect(sx, sy, dx, dy, w, h); }
|
||||
void gr_fill_rect(int x, int y, int w, int h, uint16_t c) { g_b->fill_rect(x, y, w, h, c); gpu_hd_textures_invalidate(x, y, w, h); }
|
||||
void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h) { g_b->copy_rect(sx, sy, dx, dy, w, h); gpu_hd_textures_invalidate(dx, dy, w, h); }
|
||||
void gr_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t c) {
|
||||
g_b->draw_flat_triangle(x0, y0, x1, y1, x2, y2, c);
|
||||
hd_invalidate_triangle(x0, y0, x1, y1, x2, y2);
|
||||
g_hd_perspective = 0;
|
||||
}
|
||||
void gr_draw_gouraud_triangle(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1,
|
||||
int x2, int y2, uint16_t c2) {
|
||||
g_b->draw_gouraud_triangle(x0, y0, c0, x1, y1, c1, x2, y2, c2);
|
||||
hd_invalidate_triangle(x0, y0, x1, y1, x2, y2);
|
||||
g_hd_perspective = 0;
|
||||
}
|
||||
void gr_draw_textured_triangle(int x0, int y0, int u0, int v0, int x1, int y1, int u1, int v1,
|
||||
int x2, int y2, int u2, int v2,
|
||||
uint16_t clut_x, uint16_t clut_y, uint16_t texpage) {
|
||||
hd_observe_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,clut_x,clut_y,texpage);
|
||||
g_b->draw_textured_triangle(x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2,
|
||||
clut_x, clut_y, texpage);
|
||||
hd_invalidate_triangle(x0, y0, x1, y1, x2, y2);
|
||||
g_hd_perspective = 0;
|
||||
}
|
||||
void gr_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0, uint32_t c0,
|
||||
int x1, int y1, int u1, int v1, uint32_t c1,
|
||||
int x2, int y2, int u2, int v2, uint32_t c2,
|
||||
uint16_t clut_x, uint16_t clut_y,
|
||||
uint16_t texpage, int raw) {
|
||||
hd_observe_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,clut_x,clut_y,texpage);
|
||||
g_b->draw_shaded_textured_triangle(x0, y0, u0, v0, c0, x1, y1, u1, v1, c1,
|
||||
x2, y2, u2, v2, c2, clut_x, clut_y, texpage, raw);
|
||||
hd_invalidate_triangle(x0, y0, x1, y1, x2, y2);
|
||||
g_hd_perspective = 0;
|
||||
}
|
||||
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t c) { g_b->draw_flat_rect(x, y, w, h, c); }
|
||||
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t c) { g_b->draw_flat_rect(x, y, w, h, c); if (w > 0 && h > 0) hd_invalidate_draw(x,y,x+w-1,y+h-1); }
|
||||
void gr_draw_textured_rect(int x, int y, int w, int h, int u, int v,
|
||||
uint16_t clut_x, uint16_t clut_y, uint16_t texpage) {
|
||||
hd_observe_rect(u,v,u+w,v+h,clut_x,clut_y,texpage);
|
||||
g_b->draw_textured_rect(x, y, w, h, u, v, clut_x, clut_y, texpage);
|
||||
if (w > 0 && h > 0) hd_invalidate_draw(x,y,x+w-1,y+h-1);
|
||||
}
|
||||
void gr_draw_textured_rect_scaled(int x, int y, int w, int h, int u0, int v0, int u1, int v1,
|
||||
uint16_t clut_x, uint16_t clut_y, uint16_t texpage) {
|
||||
hd_observe_rect(u0,v0,u1,v1,clut_x,clut_y,texpage);
|
||||
g_b->draw_textured_rect_scaled(x, y, w, h, u0, v0, u1, v1, clut_x, clut_y, texpage);
|
||||
if (w > 0 && h > 0) hd_invalidate_draw(x,y,x+w-1,y+h-1);
|
||||
}
|
||||
void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t c) { g_b->draw_line(x0, y0, x1, y1, c); }
|
||||
void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t c) { g_b->draw_line(x0, y0, x1, y1, c); hd_invalidate_triangle(x0,y0,x1,y1,x1,y1); }
|
||||
void gr_draw_shaded_line(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1) {
|
||||
g_b->draw_shaded_line(x0, y0, c0, x1, y1, c1);
|
||||
hd_invalidate_triangle(x0,y0,x1,y1,x1,y1);
|
||||
}
|
||||
int gr_render_display(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||||
return g_b->render_display(o, p, dx, dy, dw, dh);
|
||||
@@ -155,13 +214,14 @@ int gr_render_display(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||||
int gr_render_display_hires(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||||
return g_b->render_display_hires(o, p, dx, dy, dw, dh);
|
||||
}
|
||||
void gr_vram_write(int x, int y, uint16_t pixel) { g_b->vram_write(x, y, pixel); }
|
||||
void gr_vram_upload_begin(int x, int y, int w, int h) { gpu_hd_textures_invalidate(x,y,w,h); }
|
||||
void gr_vram_write(int x, int y, uint16_t pixel) { g_b->vram_write(x, y, pixel); gpu_hd_textures_invalidate(x,y,1,1); }
|
||||
uint16_t gr_vram_read(int x, int y) { return g_b->vram_read(x, y); }
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *d) { g_b->vram_transfer_in(x, y, w, h, d); }
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *d) { g_b->vram_transfer_in(x, y, w, h, d); gpu_hd_textures_track_upload(x,y,w,h,d); }
|
||||
void gr_vram_transfer_out(int x, int y, int w, int h, uint16_t *d) { g_b->vram_transfer_out(x, y, w, h, d); }
|
||||
void gr_set_draw_area(int x1, int y1, int x2, int y2){ g_b->set_draw_area(x1, y1, x2, y2); }
|
||||
void gr_get_draw_area(int *x1, int *y1, int *x2, int *y2) { g_b->get_draw_area(x1, y1, x2, y2); }
|
||||
void gr_set_draw_offset(int x, int y) { g_b->set_draw_offset(x, y); }
|
||||
void gr_set_draw_offset(int x, int y) { g_b->set_draw_offset(x, y); g_hd_offset_x=x; g_hd_offset_y=y; }
|
||||
|
||||
/* Native-wide compositor — present only on backends that supply it. */
|
||||
int gr_wide_supported(void) { return g_b->render_wide_display != 0; }
|
||||
|
||||
@@ -103,6 +103,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "launcher_device.h"
|
||||
#include "game_options.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_local_input_policy.h"
|
||||
#include "mod_session_baseline.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "mod_packages.h"
|
||||
@@ -5823,6 +5824,49 @@ static void apply_input_override_to_sio(int override_word) {
|
||||
(uint8_t)injected_type);
|
||||
}
|
||||
|
||||
/* Local host sticks for presentation-only mods (mod_plugins.h). */
|
||||
extern "C" int psx_mod_read_local_pad_sticks(uint32_t player,
|
||||
uint8_t out[4]) {
|
||||
if (!out) return 0;
|
||||
out[0] = out[1] = out[2] = out[3] = 0x80;
|
||||
|
||||
const bool player_valid = player < PSX_MAX_PLAYERS;
|
||||
const bool netplay = psx_netplay_active() != 0;
|
||||
const bool resimulating = psx_netplay_is_resimulating() != 0;
|
||||
if (!player_valid || netplay || resimulating) return 0;
|
||||
|
||||
PlayerInput& p = g_players[player];
|
||||
const bool dev_here = player == 0 && dev_any_input_enabled();
|
||||
const bool attached = p.kind == 1 || (p.kind == 2 && p.handle);
|
||||
if (!psx_mod_local_input_available(player_valid, attached || dev_here,
|
||||
netplay, resimulating))
|
||||
return 0;
|
||||
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
/* The local debug injector supplies a deterministic host-side sample for
|
||||
* camera/input tests. It is never sourced from rollback or a peer. */
|
||||
if (player == 0) {
|
||||
uint8_t injected[4];
|
||||
if (debug_server_get_axis_override(injected)) {
|
||||
std::memcpy(out, injected, sizeof(injected));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (attached) {
|
||||
/* Read the host pad mapping directly. The SIO-facing mode can be
|
||||
* digital because the game selected a digital pad; presentation mods
|
||||
* still need local axes without changing that simulation state. */
|
||||
pad_sticks_for(p, (int)player + 1, out);
|
||||
return 1;
|
||||
}
|
||||
const Uint8* keys = SDL_GetKeyboardState(NULL);
|
||||
psx_keybinds_sticks(keys, (int)player + 1, out);
|
||||
dev_any_controller_sticks(out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Capture one SIO slot's PHYSICAL host pad into a netplay/local blob. Returns
|
||||
* 1 if a device (or dev-any P1) is present; 0 leaves *out as released/
|
||||
* disconnected. `guarded` (savestate input guard) delivers neutral buttons and
|
||||
|
||||
+19
-3
@@ -27,6 +27,7 @@
|
||||
#include "psx_cycles.h"
|
||||
#include "psx_icache.h"
|
||||
#include "psx_memory.h"
|
||||
#include "pgxp.h"
|
||||
#include "render_pass.h"
|
||||
#include "render_pass_plan.h"
|
||||
#include "starvation_ring.h"
|
||||
@@ -1796,6 +1797,16 @@ uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES];
|
||||
* enhancement fills); like DMA it reaches memory whatever SR says. */
|
||||
int g_host_store_depth; /* >0 inside psx_host_write_* */
|
||||
|
||||
/* DMA and host stores have no instruction hook to carry or reset a PGXP
|
||||
* shadow. Validation on read cannot catch one that rewrites the identical
|
||||
* word: two projections that round to the same integers differ in their
|
||||
* fractions and depth, so the old shadow would be believed for a value it
|
||||
* did not produce. Drop the touched word's shadow instead. */
|
||||
static inline void pgxp_untracked_store(uint32_t phys) {
|
||||
if (g_dma_exec_depth > 0 || g_host_store_depth > 0)
|
||||
pgxp_invalidate_word(phys);
|
||||
}
|
||||
|
||||
static inline int cpu_store_isolated(void) {
|
||||
return sr_ptr && (*sr_ptr & 0x10000u) && g_dma_exec_depth == 0 &&
|
||||
g_host_store_depth == 0;
|
||||
@@ -1941,9 +1952,8 @@ void psx_write_word(uint32_t addr, uint32_t val) {
|
||||
}
|
||||
static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
g_guest_store_count++;
|
||||
/* (pgxp) plain-store shadow invalidation retired: the PGXP engine
|
||||
* validates tracked words against the actual packet word on read, so an
|
||||
* overwritten word can never be believed (docs/ENHANCEMENTS.md G1). */
|
||||
/* (pgxp) CPU stores carry their shadow through the PGXP_STORE hook;
|
||||
* DMA and host stores drop it below (pgxp_untracked_store). */
|
||||
/* IsC first, before any decode (cpu_store_isolated). */
|
||||
if (cpu_store_isolated()) { isc_store(addr, val, 4); return; }
|
||||
/* KSEG2 cache control — before physical translation. */
|
||||
@@ -2036,6 +2046,7 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
#ifdef PSX_COSIM
|
||||
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 4); }
|
||||
#endif
|
||||
pgxp_untracked_store(phys);
|
||||
ram[phys] = (uint8_t)(val);
|
||||
ram[phys + 1] = (uint8_t)(val >> 8);
|
||||
ram[phys + 2] = (uint8_t)(val >> 16);
|
||||
@@ -2068,6 +2079,7 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
| ((uint32_t)scratchpad[off + 3] << 24),
|
||||
val, 4);
|
||||
#endif
|
||||
pgxp_untracked_store(phys);
|
||||
scratchpad[off] = (uint8_t)(val);
|
||||
scratchpad[off + 1] = (uint8_t)(val >> 8);
|
||||
scratchpad[off + 2] = (uint8_t)(val >> 16);
|
||||
@@ -2169,6 +2181,7 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
|
||||
#ifdef PSX_COSIM
|
||||
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 2); }
|
||||
#endif
|
||||
pgxp_untracked_store(phys);
|
||||
ram[phys] = (uint8_t)(val);
|
||||
ram[phys + 1] = (uint8_t)(val >> 8);
|
||||
return;
|
||||
@@ -2197,6 +2210,7 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
|
||||
(uint32_t)scratchpad[off] | ((uint32_t)scratchpad[off + 1] << 8),
|
||||
(uint32_t)val, 2);
|
||||
#endif
|
||||
pgxp_untracked_store(phys);
|
||||
scratchpad[off] = (uint8_t)(val);
|
||||
scratchpad[off + 1] = (uint8_t)(val >> 8);
|
||||
return;
|
||||
@@ -2532,6 +2546,7 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
|
||||
#ifdef PSX_COSIM
|
||||
{ extern void cosim_note_ram_write(uint32_t,uint32_t); cosim_note_ram_write(phys, 1); }
|
||||
#endif
|
||||
pgxp_untracked_store(phys);
|
||||
ram[phys] = val;
|
||||
return;
|
||||
}
|
||||
@@ -2555,6 +2570,7 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
|
||||
debug_server_trace_write_check(phys, (uint32_t)scratchpad[phys - 0x1F800000u],
|
||||
(uint32_t)val, 1);
|
||||
#endif
|
||||
pgxp_untracked_store(phys);
|
||||
scratchpad[phys - 0x1F800000u] = val;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/* Framework implementation shared by title-owned HD texture pack manifests.
|
||||
* The owning feature declares directory resource "pack" and boolean options
|
||||
* "replacements" (default true) and "dump" (default false). */
|
||||
#include "mod_plugins.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static int hd_texture_option(const char* id, int fallback) {
|
||||
char value[16] = "";
|
||||
if (!psx_mod_current_option_value(id, value, sizeof(value))) return fallback;
|
||||
return strcmp(value, "true") == 0;
|
||||
}
|
||||
|
||||
static void builtin_hd_textures_activate(void) {
|
||||
(void)psx_mod_set_hd_texture_pack("pack", hd_texture_option("replacements", 1),
|
||||
hd_texture_option("dump", 0));
|
||||
}
|
||||
|
||||
PSX_MOD_CONSTRUCTOR(psx_register_builtin_hd_textures_plugin) {
|
||||
(void)psx_mod_register_activation_plugin("psx.hd-textures",
|
||||
builtin_hd_textures_activate);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef PSXRECOMP_MOD_LOCAL_INPUT_POLICY_H
|
||||
#define PSXRECOMP_MOD_LOCAL_INPUT_POLICY_H
|
||||
|
||||
/* Local presentation plugins may inspect live host input only while its
|
||||
* controller is available during ordinary offline execution. Guest pad mode
|
||||
* is deliberately absent: a game selecting digital SIO does not make its
|
||||
* locally attached analog stick unavailable to presentation code. Peer input
|
||||
* and rollback replay remain simulation state, never presentation input. */
|
||||
static inline int psx_mod_local_input_available(int player_valid,
|
||||
int connected,
|
||||
int netplay_active,
|
||||
int resimulating) {
|
||||
return player_valid && connected &&
|
||||
!netplay_active && !resimulating;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -774,7 +774,7 @@ std::string canonical_resolution(const std::vector<const ModPackage*>& ordered,
|
||||
out << resource.format << ':' << resource.bytes->size() << ':'
|
||||
<< resource.sha256;
|
||||
else
|
||||
out << resource.path.string();
|
||||
out << resource.path.u8string();
|
||||
out << '\n';
|
||||
}
|
||||
return out.str();
|
||||
@@ -870,7 +870,7 @@ fs::path effective_resource_path(const ModPackage& package,
|
||||
if (!selected) return {};
|
||||
const auto resource = selected->resources.find(id);
|
||||
return resource == selected->resources.end() ? fs::path{} :
|
||||
fs::path(resource->second);
|
||||
fs::u8path(resource->second);
|
||||
}
|
||||
|
||||
bool prospective_feature_enabled(
|
||||
@@ -3206,7 +3206,7 @@ bool ModPackageManager::set_feature_resource_path(
|
||||
return false;
|
||||
}
|
||||
if (!resource->shared_source.empty()) sources_[resource->shared_source] = path;
|
||||
else selections_[package_id].features[feature_id].resources[resource_id] = path.string();
|
||||
else selections_[package_id].features[feature_id].resources[resource_id] = path.u8string();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3458,7 +3458,7 @@ bool ModPackageManager::prepare_resources(const std::string& game_id,
|
||||
const auto* resource = find_resource(*package, feature.id, name);
|
||||
if (!resource || resource->input_only || resource->sha256.empty())
|
||||
throw std::runtime_error("preparer returned an undeclared or unverified output");
|
||||
pending[id].features[feature.id].resources[name] = path.string();
|
||||
pending[id].features[feature.id].resources[name] = path.u8string();
|
||||
}
|
||||
for (const auto& r : package->resources)
|
||||
if (r.feature_id == feature.id && !r.input_only && r.required && !outputs.count(r.id))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "mod_packages.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "psx_memory.h"
|
||||
#include "render_pass_projection.h"
|
||||
#include "psx_sha256.h"
|
||||
@@ -1021,7 +1022,7 @@ int provider_feature_resource_get(void*, const char* package_id,
|
||||
copy_text(out->label, sizeof(out->label), resource.label);
|
||||
copy_text(out->description, sizeof(out->description),
|
||||
resource.description);
|
||||
copy_text(out->path, sizeof(out->path), path.string());
|
||||
copy_text(out->path, sizeof(out->path), path.u8string());
|
||||
copy_text(out->status, sizeof(out->status),
|
||||
path.empty() ? "Not selected" :
|
||||
(verified ? "Selected" : "Selected path is missing"));
|
||||
@@ -1045,7 +1046,7 @@ int provider_feature_resource_set_path(void*, const char* package_id,
|
||||
return mutate([&](std::string& error) {
|
||||
return state().manager.set_feature_resource_path(
|
||||
package_id, feature_id, resource_id,
|
||||
std::filesystem::path(path), &error);
|
||||
std::filesystem::u8path(path), &error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1287,6 +1288,43 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
|
||||
"psxrecomp: mod selection kept but inactive: %s is not "
|
||||
"in this build's mod catalog\n",
|
||||
dormant.c_str());
|
||||
/* psx.hd-textures has a shared directory-resource contract, independent of
|
||||
* the title package id. Keep owner files outside the build-owned bundled
|
||||
* tree, and give the launcher a useful Open folder action on first use. */
|
||||
std::string texture_game_id = game_id;
|
||||
for (char& c : texture_game_id)
|
||||
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-' && c != '_')
|
||||
c = '_';
|
||||
if (texture_game_id.empty()) texture_game_id = "game";
|
||||
for (const auto& entry : s.manager.packages()) {
|
||||
const ModPackage* package = s.manager.selected_package(entry.first);
|
||||
if (!package || !std::any_of(package->targets.begin(), package->targets.end(),
|
||||
[&](const ModTarget& target) {
|
||||
return target.game_id == game_id || target.game_id == "*";
|
||||
})) continue;
|
||||
for (const ModPlugin& plugin : package->plugins) {
|
||||
if (plugin.id != "psx.hd-textures" ||
|
||||
!s.manager.feature_resource_path(package->id, plugin.feature_id, "pack").empty())
|
||||
continue;
|
||||
const auto resource = std::find_if(package->resources.begin(), package->resources.end(),
|
||||
[&](const ModResource& r) {
|
||||
return r.feature_id == plugin.feature_id && r.id == "pack" &&
|
||||
(r.format == "directory" || r.format == "folder");
|
||||
});
|
||||
if (resource == package->resources.end()) continue;
|
||||
std::error_code ec;
|
||||
const auto pack = std::filesystem::absolute(
|
||||
root / "texture-packs" / texture_game_id, ec);
|
||||
if (!ec) std::filesystem::create_directories(pack / "replacements", ec);
|
||||
if (!ec) std::filesystem::create_directories(pack / "dumps", ec);
|
||||
std::string directory_error;
|
||||
if (ec || !s.manager.set_feature_resource_path(package->id,
|
||||
plugin.feature_id, "pack", pack, &directory_error)) {
|
||||
std::fprintf(stderr, "psxrecomp: texture pack folder: %s\n",
|
||||
ec ? ec.message().c_str() : directory_error.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sha256_file(exe_path, s.exe_sha256, &s.error)) {
|
||||
/* Release installs commonly do not carry a loose PS-X EXE; game-id and
|
||||
* expected-byte guards remain available in that case. */
|
||||
@@ -2129,6 +2167,7 @@ extern "C" void mod_runtime_activate_plugins(void) {
|
||||
psx_projection_reset_session();
|
||||
gpu_ws_set_native_scene_predicate(nullptr);
|
||||
psx_ram_reset_size_request();
|
||||
gpu_hd_textures_shutdown();
|
||||
if (!s.initialized || !s.plan.ok) return;
|
||||
s.disc_extents.clear();
|
||||
s.audio_tracks.clear();
|
||||
@@ -2194,7 +2233,7 @@ extern "C" int psx_mod_current_resource_path(const char* resource_id,
|
||||
resource.feature_id != s.current_plugin->feature_id ||
|
||||
resource.id != resource_id)
|
||||
continue;
|
||||
const std::string text = resource.path.string();
|
||||
const std::string text = resource.path.u8string();
|
||||
if (text.empty() || text.size() + 1 > (size_t)out_size) return 0;
|
||||
std::memcpy(out, text.c_str(), text.size() + 1);
|
||||
return 1;
|
||||
@@ -2202,6 +2241,49 @@ extern "C" int psx_mod_current_resource_path(const char* resource_id,
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_current_option_value(const char* option_id,
|
||||
char* out, uint32_t out_size) {
|
||||
using namespace PSXRecompV4;
|
||||
if (out && out_size) out[0] = '\0';
|
||||
const auto* plugin = state().current_plugin;
|
||||
if (!plugin) return 0;
|
||||
return psx_mod_option_value(plugin->package_id.c_str(), plugin->feature_id.c_str(),
|
||||
option_id, out, out_size);
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_set_hd_texture_pack(const char* resource_id,
|
||||
int replacements_enabled, int dump_enabled) {
|
||||
char root[4096] = "";
|
||||
char error[512] = "";
|
||||
if (!psx_mod_current_resource_path(resource_id, root, sizeof(root))) {
|
||||
std::fprintf(stderr, "psxrecomp: HD textures require a selected pack folder\n");
|
||||
return 0;
|
||||
}
|
||||
if (!gpu_hd_textures_configure(root, replacements_enabled, dump_enabled,
|
||||
error, sizeof(error))) {
|
||||
std::fprintf(stderr, "psxrecomp: HD textures: %s\n", error);
|
||||
return 0;
|
||||
}
|
||||
std::fprintf(stdout, "psxrecomp: HD textures: %s (replacements %s, dumping %s)\n",
|
||||
root, replacements_enabled ? "on" : "off", dump_enabled ? "on" : "off");
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_set_hd_texture_dump(int enabled) {
|
||||
GpuHdTextureDiag info{};
|
||||
gpu_hd_textures_get_diag(&info);
|
||||
if (!info.root || !info.root[0]) return 0;
|
||||
gpu_hd_textures_set_dump_enabled(enabled);
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_reload_hd_texture_pack(void) {
|
||||
char error[512] = "";
|
||||
const int ok = gpu_hd_textures_reload(error, sizeof(error));
|
||||
if (!ok) std::fprintf(stderr, "psxrecomp: HD textures: %s\n", error);
|
||||
return ok;
|
||||
}
|
||||
|
||||
extern "C" uint8_t psx_mod_read_byte(uint32_t address) {
|
||||
return psx_read_byte(address);
|
||||
}
|
||||
|
||||
+32
-22
@@ -17,14 +17,15 @@
|
||||
* halves); the GPU asks for the precise position of a GP0 vertex word by the
|
||||
* packet's RAM address.
|
||||
*
|
||||
* The single safety invariant: a shadow is only ever BELIEVED after
|
||||
* validation against the actual guest word it claims to describe. Anything
|
||||
* that writes guest state without a hook (DMA, memcpy loaders, un-hooked
|
||||
* instructions) simply leaves a stale shadow behind, and the next validation
|
||||
* drops it. We never model side effects — overwrite and validate only. The
|
||||
* one accepted hole (shared with the reference implementations): an untracked
|
||||
* writer storing the byte-identical word keeps the shadow alive, which is
|
||||
* harmless because the position it describes is still that word.
|
||||
* The safety invariant: a shadow is only ever BELIEVED after validation
|
||||
* against the actual guest word it claims to describe, and only while no
|
||||
* writer it did not see has replaced that word. A matching word alone does
|
||||
* not prove provenance: two projections that round to the same integer X/Y
|
||||
* carry different fractions and depths. So every CPU write of a GPR or
|
||||
* memory word runs a hook that carries or resets its shadow, and DMA / host
|
||||
* stores drop the shadow of each word they touch (pgxp_invalidate_word, from
|
||||
* memory.c). Validation still catches any other writer that changes the
|
||||
* word. We never model side effects — overwrite, invalidate, validate.
|
||||
*
|
||||
* Everything here is host-only and visual-only: guest-visible state is never
|
||||
* read back from shadows, shadows are dropped on savestate/rewind, and the
|
||||
@@ -472,6 +473,12 @@ static inline void pv_reset(PGXPValue *pv, uint32_t value) {
|
||||
|
||||
static inline void pv_kill(PGXPValue *pv) { pv->gen = 0; }
|
||||
|
||||
extern "C" void pgxp_invalidate_word(uint32_t addr) {
|
||||
if (!g_pgxp_active) return;
|
||||
PGXPValue *pv = pgxp_ptr_w(addr); /* a render pass rolls it back */
|
||||
if (pv) pv_kill(pv);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Instruction field helpers */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@@ -1232,7 +1239,7 @@ static void alu_halves(uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2
|
||||
}
|
||||
return;
|
||||
}
|
||||
default: /* SLTI/SLTIU: not a vertex */
|
||||
default: /* SLTI/SLTIU, MFC0/CFC0: reset */
|
||||
pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
@@ -1608,7 +1615,11 @@ static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y,
|
||||
int32_t *x16, int32_t *y16,
|
||||
uint16_t *sz, int probe) {
|
||||
s_stats.lookups++;
|
||||
/* A probe asks the same question without recording it (the rectangle
|
||||
* shortcut runs one per corner before drawing): it skips every counter
|
||||
* instead of copying the whole statistics record around the call. */
|
||||
const int count = !probe;
|
||||
if (count) s_stats.lookups++;
|
||||
|
||||
int32_t px = 0, py = 0;
|
||||
uint16_t pz = 0;
|
||||
@@ -1625,7 +1636,7 @@ static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word,
|
||||
pz = (pv->flags & PGXP_F_VZ) ? pv->z : 0;
|
||||
pflags = pv->flags;
|
||||
have = PGXP_SRC_DATAFLOW;
|
||||
} else {
|
||||
} else if (count) {
|
||||
s_stats.value_mismatch++;
|
||||
}
|
||||
}
|
||||
@@ -1652,25 +1663,27 @@ static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word,
|
||||
* tolerance clamp. */
|
||||
const int why = pgxp_accept(px, py, int_x, int_y, packet_word, pflags);
|
||||
if (why == 1) {
|
||||
s_stats.trunc_reject++;
|
||||
if (count) s_stats.trunc_reject++;
|
||||
have = 0;
|
||||
} else if (why == 2) {
|
||||
s_stats.tolerance_reject++;
|
||||
if (count) s_stats.tolerance_reject++;
|
||||
have = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!have) {
|
||||
s_stats.native++;
|
||||
if (count) s_stats.native++;
|
||||
*x16 = int_x << 16;
|
||||
*y16 = int_y << 16;
|
||||
*sz = 0;
|
||||
return PGXP_SRC_NATIVE;
|
||||
}
|
||||
|
||||
if (have == PGXP_SRC_DATAFLOW) s_stats.dataflow_hit++;
|
||||
else s_stats.fallback_hit++;
|
||||
if (pz != 0) s_stats.w_valid++;
|
||||
if (count) {
|
||||
if (have == PGXP_SRC_DATAFLOW) s_stats.dataflow_hit++;
|
||||
else s_stats.fallback_hit++;
|
||||
if (pz != 0) s_stats.w_valid++;
|
||||
}
|
||||
*x16 = px;
|
||||
*y16 = py;
|
||||
*sz = pz;
|
||||
@@ -1687,13 +1700,10 @@ extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
|
||||
extern "C" int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y) {
|
||||
const PGXPStats saved = s_stats;
|
||||
int32_t x16, y16;
|
||||
uint16_t sz;
|
||||
const int src = pgxp_get_precise_vertex_impl(addr, packet_word, int_x, int_y,
|
||||
&x16, &y16, &sz, 1);
|
||||
s_stats = saved;
|
||||
return src;
|
||||
return pgxp_get_precise_vertex_impl(addr, packet_word, int_x, int_y,
|
||||
&x16, &y16, &sz, 1);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
+18
-7
@@ -55,8 +55,10 @@ static void png_chunk(FILE *f, const char *type, const uint8_t *data, size_t len
|
||||
if (len) fwrite(data, 1, len, f);
|
||||
png_put_be32(f, crc ^ 0xFFFFFFFFu);
|
||||
}
|
||||
/* Write an RGB (3 bytes/pixel, top-down) buffer as a PNG. Returns 1 on success. */
|
||||
static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||||
/* Write top-down RGB/RGBA data. This internal helper has static linkage so
|
||||
* the public wrappers stay usable by the existing C debug servers. */
|
||||
static int png_write_channels(FILE *f, const uint8_t *pixels, uint32_t w,
|
||||
uint32_t h, unsigned channels) {
|
||||
static const uint8_t sig[8] = { 137,80,78,71,13,10,26,10 };
|
||||
fwrite(sig, 1, 8, f);
|
||||
|
||||
@@ -64,20 +66,20 @@ static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||||
ihdr[0]=(uint8_t)(w>>24); ihdr[1]=(uint8_t)(w>>16); ihdr[2]=(uint8_t)(w>>8); ihdr[3]=(uint8_t)w;
|
||||
ihdr[4]=(uint8_t)(h>>24); ihdr[5]=(uint8_t)(h>>16); ihdr[6]=(uint8_t)(h>>8); ihdr[7]=(uint8_t)h;
|
||||
ihdr[8]=8; /* bit depth */
|
||||
ihdr[9]=2; /* color type 2 = truecolor RGB */
|
||||
ihdr[9]=(uint8_t)(channels == 4 ? 6 : 2); /* RGBA or RGB */
|
||||
ihdr[10]=0; /* compression */
|
||||
ihdr[11]=0; /* filter */
|
||||
ihdr[12]=0; /* interlace */
|
||||
png_chunk(f, "IHDR", ihdr, sizeof ihdr);
|
||||
|
||||
/* Filtered raw scanlines: each row prefixed with filter byte 0 (None). */
|
||||
size_t raw_len = (size_t)h * (1 + (size_t)w * 3);
|
||||
size_t raw_len = (size_t)h * (1 + (size_t)w * channels);
|
||||
uint8_t *raw = (uint8_t *)malloc(raw_len);
|
||||
if (!raw) return 0;
|
||||
for (uint32_t y = 0; y < h; y++) {
|
||||
uint8_t *row = raw + (size_t)y * (1 + (size_t)w * 3);
|
||||
uint8_t *row = raw + (size_t)y * (1 + (size_t)w * channels);
|
||||
row[0] = 0;
|
||||
memcpy(row + 1, rgb + (size_t)y * w * 3, (size_t)w * 3);
|
||||
memcpy(row + 1, pixels + (size_t)y * w * channels, (size_t)w * channels);
|
||||
}
|
||||
|
||||
/* zlib stream: 2-byte header + stored DEFLATE blocks + 4-byte Adler32. */
|
||||
@@ -106,7 +108,16 @@ static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||||
png_chunk(f, "IDAT", z, zi);
|
||||
free(z);
|
||||
png_chunk(f, "IEND", NULL, 0);
|
||||
return 1;
|
||||
return !ferror(f);
|
||||
}
|
||||
|
||||
/* Write an RGB (3 bytes/pixel, top-down) buffer as a PNG. Returns 1 on success. */
|
||||
static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||||
return png_write_channels(f, rgb, w, h, 3);
|
||||
}
|
||||
/* Preserve raw 8-bit RGBA, including DuckStation's ST alpha convention. */
|
||||
static int png_write_rgba(FILE *f, const uint8_t *rgba, uint32_t w, uint32_t h) {
|
||||
return png_write_channels(f, rgba, w, h, 4);
|
||||
}
|
||||
|
||||
#endif /* PSX_PNG_WRITE_H */
|
||||
|
||||
@@ -387,6 +387,7 @@ static void exec_one(CPUState* cpu) {
|
||||
switch (cop_op) {
|
||||
case 0x00: /* MFC0 */
|
||||
set_load_delay(RT(insn), cpu->cop0[RD(insn)]);
|
||||
psx_pgxp_alu(cpu, insn, cpu->cop0[RD(insn)], 0, 0);
|
||||
break;
|
||||
case 0x04: /* MTC0 */
|
||||
cpu->cop0[RD(insn)] = rt_val;
|
||||
|
||||
@@ -357,6 +357,7 @@ int gr_render_display_hires(uint32_t *out, int pitch, int dx, int dy, int dw, in
|
||||
(void)out; (void)pitch; (void)dx; (void)dy; (void)dw; (void)dh;
|
||||
return 0;
|
||||
}
|
||||
void gr_vram_upload_begin(int x,int y,int w,int h) { (void)x; (void)y; (void)w; (void)h; }
|
||||
void gr_vram_write(int x, int y, uint16_t pixel) { (void)x; (void)y; (void)pixel; }
|
||||
uint16_t gr_vram_read(int x, int y) { (void)x; (void)y; return 0; }
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Existing GL fixtures deliberately run without any external pack. The HD
|
||||
* fixture links the real session adapter instead. */
|
||||
#ifndef PSX_TEST_HD_TEXTURE_PACK
|
||||
int gpu_hd_textures_acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy,
|
||||
const int limits[4], uint32_t window, int semi, GpuHdTextureImage* image) {
|
||||
(void)tp; (void)cx; (void)cy; (void)limits; (void)window; (void)semi;
|
||||
memset(image, 0, sizeof(*image)); return 0;
|
||||
}
|
||||
void gpu_hd_textures_release_image(GpuHdTextureImage* image) {
|
||||
memset(image, 0, sizeof(*image));
|
||||
}
|
||||
void gpu_hd_textures_note_applied(void) {}
|
||||
void gpu_hd_textures_reset_tracking(void) {}
|
||||
#endif
|
||||
@@ -26,6 +26,11 @@ void (*g_overlay_flush_pending_cycles)(void);
|
||||
uint32_t g_psx_cyc_batch;
|
||||
uint32_t g_psx_cyc_batch_limit;
|
||||
uint32_t *g_psx_cyc_local_acc;
|
||||
#ifndef PSX_STUBS_REAL_PGXP
|
||||
/* memory.c drops the PGXP shadow of a DMA / host store (pgxp.cpp).
|
||||
* pgxp_untracked_store_test links the real engine. */
|
||||
void pgxp_invalidate_word(uint32_t addr) { (void)addr; }
|
||||
#endif
|
||||
#ifndef PSX_STUBS_REAL_ICACHE
|
||||
/* The I-cache model (psx_icache.c). isc_store_test links the real one. */
|
||||
uint32_t g_psx_icache_tv[1024];
|
||||
@@ -170,3 +175,4 @@ void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *d) {
|
||||
(void)x; (void)y; memcpy(s_vram, d, (size_t)w * (size_t)h * 2u);
|
||||
g_stub_device_restores++;
|
||||
}
|
||||
void gr_vram_upload_begin(int x,int y,int w,int h) { (void)x; (void)y; (void)w; (void)h; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Compile and check palette-aware minification on a hidden real GL context."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import subprocess
|
||||
@@ -13,14 +14,19 @@ def main():
|
||||
parser.add_argument('--sdl-library',required=True)
|
||||
parser.add_argument('--output',required=True)
|
||||
parser.add_argument('--fixture',type=Path)
|
||||
parser.add_argument('--hd-pack', action='store_true')
|
||||
parser.add_argument('--cxx')
|
||||
parser.add_argument('--argument', action='append', default=[])
|
||||
args=parser.parse_args()
|
||||
fw=Path(__file__).resolve().parents[2]
|
||||
out=Path(args.output).resolve(); out.mkdir(parents=True,exist_ok=True)
|
||||
receipt=[]
|
||||
def run(command):
|
||||
def run(command,environment=None):
|
||||
command=[str(x) for x in command]
|
||||
r=subprocess.run(command,cwd=out,capture_output=True,text=True,encoding='utf-8',errors='replace')
|
||||
receipt.append(dict(cmd=command,exit=r.returncode,stdout=r.stdout,stderr=r.stderr))
|
||||
env=os.environ.copy()
|
||||
if environment: env.update(environment)
|
||||
r=subprocess.run(command,cwd=out,capture_output=True,text=True,encoding='utf-8',errors='replace',env=env)
|
||||
receipt.append(dict(cmd=command,env=environment,exit=r.returncode,stdout=r.stdout,stderr=r.stderr))
|
||||
(out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n',encoding='utf-8')
|
||||
print(r.stdout,r.stderr[-4000:],flush=True)
|
||||
if r.returncode: raise subprocess.CalledProcessError(r.returncode,command)
|
||||
@@ -28,7 +34,7 @@ def main():
|
||||
'-ffunction-sections','-fdata-sections','-I',fw/'runtime/include','-I',fw/'runtime/src']
|
||||
for include in args.sdl_include.split(';'):
|
||||
if include: flags+=['-I',include]
|
||||
sources=[('probe',args.fixture or fw/'runtime/tests/test_gl_texture_filter.c'),
|
||||
sources=[('probe',args.fixture.resolve() if args.fixture else fw/'runtime/tests/test_gl_texture_filter.c'),
|
||||
('sw',fw/'runtime/src/gpu_sw_renderer.c'),('fi',fw/'runtime/src/frame_interpolation.c'),
|
||||
('rp',fw/'runtime/src/render_pass_plan.c')]
|
||||
if (fw/'runtime/src/psx_openxr.c').exists():
|
||||
@@ -36,6 +42,13 @@ def main():
|
||||
objects=[]
|
||||
for name,source in sources:
|
||||
obj=out/(name+'.o'); run([args.cc,*flags,'-c',source,'-o',obj]); objects.append(obj)
|
||||
if args.hd_pack:
|
||||
if not args.cxx: parser.error('--hd-pack requires --cxx')
|
||||
cppflags=[arg if arg!='-std=gnu11' else '-std=c++17' for arg in flags]
|
||||
for name in ('gpu_hd_textures','hd_texture_pack','duckstation_texture_pack'):
|
||||
obj=out/(name+'.o')
|
||||
run([args.cxx,*cppflags,'-c',fw/'runtime/src'/(name+'.cpp'),'-o',obj])
|
||||
objects.append(obj)
|
||||
exe=out/('filter.exe' if platform.system()=='Windows' else 'filter')
|
||||
libraries=['-lm']
|
||||
if platform.system()=='Windows':
|
||||
@@ -47,8 +60,15 @@ def main():
|
||||
libraries+=['-liconv','-Wl,-dead_strip']
|
||||
else: libraries+=['-lGL','-ldl','-lpthread']
|
||||
if platform.system()!='Darwin': libraries+=['-Wl,--gc-sections']
|
||||
run([args.cc,*objects,args.sdl_library,'-o',exe,*libraries])
|
||||
run([exe])
|
||||
run([args.cxx if args.hd_pack else args.cc,*objects,args.sdl_library,'-o',exe,*libraries])
|
||||
arguments=list(args.argument)
|
||||
if args.hd_pack:
|
||||
pack=out/'pack'; (pack/'replacements').mkdir(parents=True,exist_ok=True)
|
||||
(pack/'beetle'/'demo-texture-replacements').mkdir(parents=True,exist_ok=True)
|
||||
arguments=[pack]
|
||||
run([exe,*arguments],{'PSX_GL_HIRES_WINDOW':'0'} if args.hd_pack else None)
|
||||
if args.hd_pack: run([exe,*arguments],{'PSX_GL_HIRES_WINDOW':'1'})
|
||||
if args.hd_pack: run([exe,'--native-baseline'],{'PSX_GL_HIRES_WINDOW':'1'})
|
||||
return 0
|
||||
|
||||
if __name__=='__main__':
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "../third_party/stb_image.h"
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "../src/png_write.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
int failures = 0;
|
||||
void check(bool ok, const char* what) {
|
||||
if (!ok) { std::fprintf(stderr,"FAIL: %s\n",what); ++failures; }
|
||||
}
|
||||
using Pack = std::unique_ptr<DuckTexturePack,decltype(&duck_texture_pack_destroy)>;
|
||||
Pack create(const fs::path& root) {
|
||||
DuckTexturePack* pack = nullptr; char error[512]{};
|
||||
check(duck_texture_pack_create(root.u8string().c_str(),&pack,error,sizeof(error)) == 1,error);
|
||||
return Pack(pack,&duck_texture_pack_destroy);
|
||||
}
|
||||
std::vector<uint16_t> words(size_t n) {
|
||||
std::vector<uint16_t> result(n);
|
||||
for (size_t i = 0; i < n; ++i) result[i] = static_cast<uint16_t>((i*7919)^0xa51c);
|
||||
return result;
|
||||
}
|
||||
void place(std::vector<uint16_t>& vram, unsigned x, unsigned y, unsigned w,
|
||||
const std::vector<uint16_t>& source) {
|
||||
for (size_t i = 0; i < source.size(); ++i) vram[(y+i/w)*1024+x+i%w] = source[i];
|
||||
}
|
||||
HdTextureDrawQuery query(std::vector<uint16_t>& vram, uint16_t page_x, uint16_t page_y,
|
||||
uint8_t depth, uint8_t u0, uint8_t u1, uint8_t v0, uint8_t v1,
|
||||
uint16_t clut_x = 512, uint16_t clut_y = 400) {
|
||||
return {page_x,page_y,depth,u0,u1,v0,v1,clut_x,clut_y,vram.data(),vram.size()};
|
||||
}
|
||||
void png(const fs::path& path, unsigned w = 4, unsigned h = 4) {
|
||||
fs::create_directories(path.parent_path());
|
||||
std::vector<uint8_t> pixels(size_t(w)*h*4);
|
||||
constexpr uint8_t alpha[] = {0,127,128,242,243,255};
|
||||
for (size_t i = 0; i < size_t(w)*h; ++i) {
|
||||
pixels[i*4] = 23; pixels[i*4+1] = 57; pixels[i*4+2] = 91; pixels[i*4+3] = alpha[i%6];
|
||||
}
|
||||
#ifdef _WIN32
|
||||
FILE* file = _wfopen(path.c_str(),L"wb");
|
||||
#else
|
||||
FILE* file = std::fopen(path.c_str(),"wb");
|
||||
#endif
|
||||
check(file != nullptr,"open original test PNG");
|
||||
if (file) { check(png_write_rgba(file,pixels.data(),w,h) == 1,"encode original test PNG"); std::fclose(file); }
|
||||
}
|
||||
DuckTexturePixels decoded(DuckTexturePack* pack, uint64_t id) {
|
||||
DuckTexturePixels pixels{};
|
||||
check(duck_texture_pack_request_decode(pack,id) >= 0,"queue PNG decode");
|
||||
const auto end = std::chrono::steady_clock::now()+std::chrono::seconds(5);
|
||||
int status = HD_TEXTURE_LOOKUP_NONE;
|
||||
do {
|
||||
status = duck_texture_pack_acquire_decoded(pack,id,&pixels);
|
||||
if (status != HD_TEXTURE_LOOKUP_NONE) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
} while (std::chrono::steady_clock::now() < end);
|
||||
check(status == HD_TEXTURE_LOOKUP_FOUND,"background PNG decode becomes ready");
|
||||
return pixels;
|
||||
}
|
||||
bool decode_fails(DuckTexturePack* pack, uint64_t id) {
|
||||
int status = duck_texture_pack_request_decode(pack,id);
|
||||
const auto end = std::chrono::steady_clock::now()+std::chrono::seconds(5);
|
||||
while (status == HD_TEXTURE_LOOKUP_NONE && std::chrono::steady_clock::now() < end) {
|
||||
DuckTexturePixels pixels{};
|
||||
status = duck_texture_pack_acquire_decoded(pack,id,&pixels);
|
||||
duck_texture_pixels_release(&pixels);
|
||||
if (status == HD_TEXTURE_LOOKUP_NONE) std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
return status == HD_TEXTURE_LOOKUP_ERROR;
|
||||
}
|
||||
|
||||
void fixed_hash_vectors() {
|
||||
/* Independently produced by Python xxhash 3.6.0's native wheel. These
|
||||
* literal expectations cover XXH3's short/medium/long-input boundaries. */
|
||||
const std::pair<size_t,uint64_t> vectors[] = {
|
||||
{0,0x2d06800538d394c2ULL},{1,0xc5779b2f56fbe2e1ULL},{2,0x0edb68960f55aef6ULL},
|
||||
{3,0xfce9d54aec3f9d32ULL},{4,0x3631d2a04ec85311ULL},{8,0x000ca76cb9b329dcULL},
|
||||
{9,0xab6a9091cf532d8aULL},{16,0x9cbcdea9305d7528ULL},{17,0x2f133c3e193cbe48ULL},
|
||||
{32,0x3e4a087da5cd5990ULL},{64,0xc3e3ef9ad7f08dacULL},{65,0x740c4e7d1690d606ULL},
|
||||
{120,0x5942d561231419b2ULL},{121,0x152cb5857c4535f3ULL},{128,0x351cd56a8fe50e66ULL},
|
||||
{129,0x38a45de7ae84906eULL},{256,0x0005d0b83a005f4eULL},{8192,0xc68a00dcca652920ULL}
|
||||
};
|
||||
for (const auto& v : vectors) {
|
||||
const auto input = words(v.first);
|
||||
check(duck_texture_hash_words_le(input.data(),input.size()) == v.second,"fixed independent LE16 XXH3 vector");
|
||||
}
|
||||
std::vector<uint16_t> vram(1024*512,0xf00d);
|
||||
place(vram,101,39,2,words(8));
|
||||
check(duck_texture_hash_rect(vram.data(),vram.size(),101,39,2,4) == 0x000ca76cb9b329dcULL,
|
||||
"strided native rectangle excludes unrelated VRAM words");
|
||||
check(duck_texture_hash_rect(vram.data(),vram.size(),1023,0,2,1) == 0,"wrapped hash rectangle rejected");
|
||||
}
|
||||
|
||||
void names() {
|
||||
DuckTextureKey key{};
|
||||
check(duck_texture_parse_name("texupload-P4-0123456789abcdef-FEDCBA9876543210-64x256-0-192-64x64-P0-14.png",&key) == 1,
|
||||
"literal upstream documented upload filename");
|
||||
check(key.source_hash == 0x0123456789abcdefULL && key.palette_hash == 0xfedcba9876543210ULL &&
|
||||
key.source_width_words == 64 && key.offset_y == 192 && key.width == 64 && key.palette_max == 14,
|
||||
"filename dimensions distinguish source words from expanded texels");
|
||||
check(duck_texture_parse_name("texpage-STC16-0123456789ABCDEF-256x256-8-16-32x64.PNG",&key) == 1 &&
|
||||
key.kind == DUCK_TEXTURE_PAGE && key.semitransparent && key.depth == 2 && !key.palette_hash,
|
||||
"direct ST filename omits palette fields");
|
||||
const char* invalid[] = {
|
||||
"texpage-P4-12345678-0123456789ABCDEF-64x256-0-0-4x1-P0-15.png",
|
||||
"texpage-P4-0123456789ABCDEF-0123456789ABCDEF-64x256-0-0-3x1-P0-15.png",
|
||||
"texpage-P4-0123456789ABCDEF-0123456789ABCDEF-64x256-0-0-4x1-P0-16.png",
|
||||
"texpage-P8-0123456789ABCDEF-0123456789ABCDEF-128x256-0-0-4x1-P15-0.png",
|
||||
"texupload-C16-0123456789ABCDEF-0x256-0-0-4x1.png",
|
||||
"texupload-C16-0123456789ABCDEF-65536x1-0-0-4x1.png",
|
||||
"texpage-C16-0123456789ABCDEF-256x256-255-0-2x1.png",
|
||||
"texpage-C16-0123456789ABCDEF-256x256-0-0-4x1.png.old",
|
||||
"../texpage-C16-0123456789ABCDEF-256x256-0-0-4x1.png"
|
||||
};
|
||||
for (const char* name : invalid) check(!duck_texture_parse_name(name,&key),"targeted malformed/unsupported name rejected");
|
||||
}
|
||||
|
||||
void page_and_decode(const fs::path& root) {
|
||||
const char* name = "texpage-P4-3631D2A04EC85311-9CBCDEA9305D7528-64x256-4-2-8x2-P0-15.png";
|
||||
png(root/"replacements"/"nested"/name,16,4);
|
||||
std::vector<uint16_t> vram(1024*512);
|
||||
place(vram,65,258,2,words(4)); place(vram,512,400,16,words(16));
|
||||
auto pack = create(root); auto q = query(vram,64,256,0,5,10,2,3);
|
||||
DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"fixed subpage fixture matches");
|
||||
check(match.origin_u == 4 && match.origin_v == 2 && match.source_width == 8 && match.source_height == 2,
|
||||
"crop extent and origin are page-space texels");
|
||||
auto pixels = decoded(pack.get(),match.entry_id);
|
||||
check(pixels.width == 16 && pixels.height == 4,"arbitrary PNG scaling uses image dimensions");
|
||||
constexpr uint8_t alpha[] = {0,127,128,242,243,255};
|
||||
if (pixels.rgba) for (unsigned i = 0; i < 6; ++i)
|
||||
check(pixels.rgba[i*4+3] == alpha[i],"decode retains raw alpha boundary bytes");
|
||||
const uint16_t old = vram[258*1024+65];
|
||||
vram[258*1024+65] ^= 1; duck_texture_pack_invalidate(pack.get(),65,258,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"write invalidates positive page cache");
|
||||
vram[258*1024+65] = old; duck_texture_pack_invalidate(pack.get(),65,258,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"write invalidates negative page cache");
|
||||
vram[400*1024+512] ^= 1; duck_texture_pack_invalidate(pack.get(),512,400,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"CLUT write invalidates cached match");
|
||||
pack.reset();
|
||||
check(pixels.rgba && pixels.rgba[0] == 23,"pixel lease outlives pack and decode worker");
|
||||
duck_texture_pixels_release(&pixels);
|
||||
}
|
||||
|
||||
void upload(const fs::path& root) {
|
||||
png(root/"replacements"/"texupload-C16-000CA76CB9B329DC-4x2-2-0-2x2.png",4,8);
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,66,257,4,words(8));
|
||||
auto pack = create(root); auto q = query(vram,64,256,2,4,5,1,2);
|
||||
DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"upload names require original source identity");
|
||||
check(duck_texture_pack_track_upload(pack.get(),66,257,4,2,vram.data(),vram.size()),"track post-write native upload");
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND && match.origin_u == 4 && match.origin_v == 1,
|
||||
"upload crop has correct source origin");
|
||||
duck_texture_pack_invalidate(pack.get(),66,257,1,1); /* outside replacement crop */
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"partial source overwrite drops entire upload identity");
|
||||
duck_texture_pack_track_upload(pack.get(),66,257,4,2,vram.data(),vram.size());
|
||||
duck_texture_pack_reset_tracking(pack.get());
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"savestate/reset clears upload identity");
|
||||
check(!duck_texture_pack_track_upload(pack.get(),1023,0,2,1,vram.data(),vram.size()),"wrapped upload explicitly falls back");
|
||||
}
|
||||
|
||||
void reload_tracking(const fs::path& root) {
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,66,257,4,words(8));
|
||||
auto old = create(root);
|
||||
check(duck_texture_pack_track_upload(old.get(),66,257,4,2,vram.data(),vram.size()),"capture upload before replacement file exists");
|
||||
png(root/"replacements"/"texupload-C16-000CA76CB9B329DC-4x2-2-0-2x2.png");
|
||||
auto refreshed = create(root); auto q = query(vram,64,256,2,4,5,1,2); DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"newly indexed replacement initially has no upload identity");
|
||||
check(duck_texture_pack_copy_tracking(refreshed.get(),old.get()),"same-session reload copies upload tracking");
|
||||
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"copied tracking makes newly indexed replacement immediately match");
|
||||
duck_texture_pack_invalidate(refreshed.get(),66,257,1,1);
|
||||
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"destination tracking invalidates independently");
|
||||
auto second = create(root); duck_texture_pack_copy_tracking(second.get(),old.get());
|
||||
check(duck_texture_pack_match(second.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"destination invalidation leaves original tracker intact");
|
||||
}
|
||||
|
||||
void palettes(const fs::path& root) {
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,128,0,1,words(2));
|
||||
place(vram,1008,400,16,words(16));
|
||||
const char* edge = "texpage-P8-0EDB68960F55AEF6-9CBCDEA9305D7528-128x256-0-0-2x2-P0-255.png";
|
||||
png(root/"edge"/"replacements"/edge);
|
||||
auto pack = create(root/"edge"); auto q = query(vram,128,0,1,0,1,0,1,1008,400); DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"full P8 edge CLUT hashes remaining 16 words");
|
||||
vram[400*1024] = 42; duck_texture_pack_invalidate(pack.get(),0,400,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"full P8 CLUT hash does not wrap into row start");
|
||||
const char* partial = "texpage-P8-0EDB68960F55AEF6-3631D2A04EC85311-128x256-0-0-2x2-P8-11.png";
|
||||
png(root/"partial"/"replacements"/partial);
|
||||
place(vram,512,400,16,words(16)); q.clut_x = 512;
|
||||
pack = create(root/"partial");
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"reduced P8 min8-max11 hashes prefix4 per upstream pin");
|
||||
vram[400*1024+520] ^= 1; duck_texture_pack_invalidate(pack.get(),520,400,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"pinned prefix quirk ignores palette8 despite declared minimum8");
|
||||
vram[400*1024+512] ^= 1; duck_texture_pack_invalidate(pack.get(),512,400,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"reduced P8 prefix change stops replacement");
|
||||
q.clut_x = 1023;
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"misaligned/edge reduced CLUT rejected");
|
||||
}
|
||||
|
||||
void st_alias_duplicates(const fs::path& root) {
|
||||
const char* ordinary = "texpage-C16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
||||
const char* st = "texpage-STC16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
||||
png(root/"paired"/"replacements"/ordinary); png(root/"paired"/"replacements"/st);
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,64,0,4,words(8)); auto q = query(vram,64,0,2,0,3,0,1);
|
||||
auto pack = create(root/"paired"); DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match_draw(pack.get(),&q,0,&match) == HD_TEXTURE_LOOKUP_FOUND && !match.key.semitransparent,
|
||||
"ordinary primitive prefers ordinary filename");
|
||||
check(duck_texture_pack_match_draw(pack.get(),&q,1,&match) == HD_TEXTURE_LOOKUP_FOUND && match.key.semitransparent,
|
||||
"semitransparent primitive prefers ST filename");
|
||||
png(root/"alias"/"replacements"/"art"/"sample.png");
|
||||
fs::create_directories(root/"alias");
|
||||
std::ofstream config(root/"alias"/"config.yaml");
|
||||
config << "MaxVRAMWriteCoalesceWidth: 1\nAliases:\n " << std::string(ordinary).substr(0,std::strlen(ordinary)-4)
|
||||
<< ": |\n art/sample.png\n"; config.close();
|
||||
pack = create(root/"alias");
|
||||
check(duck_texture_pack_match_draw(pack.get(),&q,1,&match) == HD_TEXTURE_LOOKUP_FOUND && !match.key.semitransparent,
|
||||
"bounded literal alias and opposite-convention fallback");
|
||||
DuckTexturePackInfo info{}; duck_texture_pack_get_info(pack.get(),&info);
|
||||
check(info.ignored_count && std::strstr(info.diagnostic,"MaxVRAMWriteCoalesceWidth"),"unsupported config option explicitly diagnosed");
|
||||
png(root/"duplicate"/"replacements"/"a"/ordinary); png(root/"duplicate"/"replacements"/"b"/ordinary);
|
||||
pack = create(root/"duplicate");
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_AMBIGUOUS,"recursive duplicate keys fall back deterministically");
|
||||
q.u_first = 254; q.u_last = 1;
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"wrapped UV interval falls back");
|
||||
}
|
||||
|
||||
void failed_payloads(const fs::path& root) {
|
||||
const char* name = "texpage-C16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
||||
png(root/"budget"/"replacements"/name);
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,64,0,4,words(8)); auto q = query(vram,64,0,2,0,3,0,1);
|
||||
auto pack = create(root/"budget"); DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"over-budget payload identity remains valid metadata");
|
||||
duck_texture_pack_set_decode_budget(pack.get(),8);
|
||||
check(decode_fails(pack.get(),match.entry_id),"over-budget decoded image fails before pixel allocation");
|
||||
fs::create_directories(root/"malformed"/"replacements");
|
||||
std::ofstream invalid(root/"malformed"/"replacements"/name,std::ios::binary); invalid << "not PNG"; invalid.close();
|
||||
pack = create(root/"malformed");
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"malformed PNG identity can be indexed without render-thread decode");
|
||||
check(decode_fails(pack.get(),match.entry_id),"malformed PNG fails asynchronously for native fallback");
|
||||
const fs::path oversized = root/"dimension"/"replacements"/name;
|
||||
png(oversized);
|
||||
std::ifstream file(oversized,std::ios::binary); std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(file)),{}); file.close();
|
||||
/* Forge a valid IHDR width above the decode dimension bound. Keeping the
|
||||
* tiny original IDAT makes the fixture inexpensive and verifies rejection
|
||||
* using image metadata before an enormous allocation could happen. */
|
||||
bytes[16] = 0; bytes[17] = 0; bytes[18] = 0x23; bytes[19] = 0x28; /* 9000 */
|
||||
const uint32_t crc = png_crc_update(0xffffffffu,bytes.data()+12,17)^0xffffffffu;
|
||||
for (unsigned i = 0; i < 4; ++i) bytes[29+i] = static_cast<uint8_t>(crc >> (24-8*i));
|
||||
std::ofstream changed(oversized,std::ios::binary|std::ios::trunc);
|
||||
changed.write(reinterpret_cast<const char*>(bytes.data()),bytes.size()); changed.close();
|
||||
pack = create(root/"dimension"); duck_texture_pack_match(pack.get(),&q,&match);
|
||||
check(decode_fails(pack.get(),match.entry_id),"oversized PNG IHDR fails within native fallback bounds");
|
||||
}
|
||||
|
||||
void dumping(const fs::path& root) {
|
||||
auto pack = create(root); std::vector<uint16_t> vram(1024*512);
|
||||
vram[0] = 0x3210; vram[400*1024+1] = 0x001f; vram[400*1024+2] = 0x8000; vram[400*1024+3] = 0xffff;
|
||||
auto q = query(vram,0,0,0,0,3,0,0,0,400); char error[512]{};
|
||||
check(duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"queue native ST page dump");
|
||||
check(duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_NONE,"dump session deduplicates without filesystem access");
|
||||
pack.reset(); /* required to finish queued background writes */
|
||||
const fs::path file = root/"dumps"/"texpage-STP4-A92BF769517E0361-59786C612FB45363-64x256-0-0-4x1-P0-15.png";
|
||||
check(fs::is_regular_file(file),"fixed independent hash filename published at worker shutdown");
|
||||
int width = 0, height = 0, channels = 0;
|
||||
std::ifstream encoded_file(file,std::ios::binary);
|
||||
std::vector<uint8_t> encoded((std::istreambuf_iterator<char>(encoded_file)),{});
|
||||
auto* image = encoded.empty() ? nullptr : stbi_load_from_memory(encoded.data(),static_cast<int>(encoded.size()),&width,&height,&channels,4);
|
||||
constexpr uint8_t expected[] = {0,0,0,0,255,0,0,255,0,0,0,128,255,255,255,128};
|
||||
check(image && width == 4 && height == 1 && !std::memcmp(image,expected,sizeof(expected)),"dump decodes native indices/CLUT and ST alpha");
|
||||
if (image) stbi_image_free(image);
|
||||
std::ofstream owner(file,std::ios::binary|std::ios::trunc); owner << "owner edit"; owner.close();
|
||||
pack = create(root); duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)); pack.reset();
|
||||
std::ifstream edited(file,std::ios::binary); std::string payload((std::istreambuf_iterator<char>(edited)),{});
|
||||
check(payload == "owner edit","existing owner dump is never overwritten");
|
||||
pack = create(root/"upload");
|
||||
duck_texture_pack_track_upload(pack.get(),0,0,1,1,vram.data(),vram.size());
|
||||
check(duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"tracked source emits texupload dump");
|
||||
pack.reset();
|
||||
check(fs::is_regular_file(root/"upload"/"dumps"/"texupload-P4-A92BF769517E0361-59786C612FB45363-1x1-0-0-4x1-P0-15.png"),
|
||||
"upload dump retains original source dimensions");
|
||||
/* PNG fixtures remain independent of the VRAM pointer used by replacement
|
||||
* matching, so dumping never reads already-replaced pixels. */
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const fs::path root = fs::temp_directory_path()/fs::u8path("psx-duck-texture-\xCE\xA9-"+
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
try {
|
||||
fixed_hash_vectors(); names(); page_and_decode(root/"page"); upload(root/"upload"); reload_tracking(root/"reload");
|
||||
palettes(root/"palettes"); st_alias_duplicates(root/"keys"); failed_payloads(root/"invalid"); dumping(root/"dump");
|
||||
/* Every artifact is created beneath this fresh, explicitly named test
|
||||
* root. No user-provided paths participate in recursive cleanup. */
|
||||
fs::remove_all(root);
|
||||
} catch (const std::exception& e) { check(false,e.what()); }
|
||||
if (failures) { std::fprintf(stderr,"test_duckstation_texture_pack: %d failure(s)\n",failures); return 1; }
|
||||
std::puts("PASS: DuckStation format, independent hashes, native matching, decode and dumps");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/* Real GL + real format/decoder + renderer facade. Source-owned fixtures. */
|
||||
#define PSX_TEST_HD_TEXTURE_PACK 1
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_ONLY_PNG
|
||||
#include "../third_party/stb_image.h"
|
||||
#define main scale_fixture_main
|
||||
#include "test_gl_scale_invariance.c"
|
||||
#undef main
|
||||
#include "gpu_render.c"
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "hd_texture_pack.h"
|
||||
#include "png_write.h"
|
||||
|
||||
const GpuRenderBackend* vk_backend_get(void) { return NULL; }
|
||||
void present_shot_done(int ok) { (void)ok; }
|
||||
int host_osd_needs_present(void) { return 0; }
|
||||
int psx_present_vsync_owns_cadence(void) { return 0; }
|
||||
void latency_ring_mark(LatencyStage stage) { (void)stage; }
|
||||
void psx_host_sleep_ms(unsigned ms) { SDL_Delay(ms); }
|
||||
int present_shot_take(char* out,int n) { (void)out; (void)n; return 0; }
|
||||
int host_osd_image(const uint32_t** p,int* w,int* h) { (void)p; (void)w; (void)h; return 0; }
|
||||
int host_osd_volume_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
void host_osd_present_done(void) {}
|
||||
int psx_rewind_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
float psx_rewind_slide(void) { return 0; }
|
||||
int psx_savestate_menu_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
|
||||
static uint16_t source_words[4*4];
|
||||
static uint16_t reference[1024*512];
|
||||
static uint8_t pixels[64*64*4];
|
||||
static const uint16_t texture_page=8u|(2u<<7);
|
||||
|
||||
static void pack_png(const char* root,int st) {
|
||||
DuckTextureKey key={0};
|
||||
key.source_hash=duck_texture_hash_words_le(source_words,16);
|
||||
key.source_width_words=4; key.source_height=4;
|
||||
key.width=4; key.height=4; key.kind=DUCK_TEXTURE_UPLOAD;
|
||||
key.depth=HD_TEXTURE_DEPTH_16BPP; key.semitransparent=st;
|
||||
char stem[256],path[2048];
|
||||
check(duck_texture_format_name(&key,stem,sizeof(stem)),"fixture filename");
|
||||
snprintf(path,sizeof(path),"%s/replacements/%s.png",root,stem);
|
||||
uint8_t rgba[16*16*4];
|
||||
for(int y=0;y<16;++y) for(int x=0;x<16;++x) {
|
||||
uint8_t* at=rgba+(y*16+x)*4;
|
||||
at[0]=(x&1)?0:255; at[1]=0; at[2]=(x&1)?255:0; at[3]=255;
|
||||
if(st) {
|
||||
const uint8_t alpha[6]={0,127,128,242,243,255};
|
||||
at[3]=alpha[x%6];
|
||||
if(y>=8) at[0]=at[1]=at[2]=0;
|
||||
} else if(y>=8) {
|
||||
at[0]=at[1]=at[2]=0;
|
||||
at[3]=(x&1)?128:127;
|
||||
}
|
||||
}
|
||||
FILE* file=fopen(path,"wb");
|
||||
check(file!=NULL,"open fixture PNG");
|
||||
if(file) { check(png_write_rgba(file,rgba,16,16),"write fixture PNG"); fclose(file); }
|
||||
}
|
||||
static void state(void) {
|
||||
gr_set_draw_area(0,0,1023,511); gr_set_draw_offset(0,0);
|
||||
gr_set_mask_bits(0,0); gr_set_semi_transparency(0,0);
|
||||
gr_set_texture_window(0); gr_set_color_modulation(128,128,128,1);
|
||||
}
|
||||
static void native_scene(void) {
|
||||
state();
|
||||
gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
gr_fill_rect(0,0,64,64,0x4210);
|
||||
gr_draw_textured_rect(16,16,4,4,0,0,0,0,texture_page);
|
||||
for(int mode=0;mode<4;++mode) {
|
||||
gr_set_semi_transparency(1,mode);
|
||||
gr_draw_textured_rect(20+mode*5,20,4,4,0,0,0,0,texture_page);
|
||||
}
|
||||
gr_set_semi_transparency(0,0);
|
||||
gr_set_mask_bits(1,0); gr_draw_flat_rect(40,40,8,8,0x001f);
|
||||
gr_set_mask_bits(0,1); gr_draw_textured_rect(38,38,12,12,0,0,0,0,texture_page);
|
||||
gr_set_mask_bits(0,0);
|
||||
gr_set_texture_filter(1);
|
||||
gr_set_perspective_triangle(1,1.0f,0.5f,0.25f);
|
||||
gr_set_precise_triangle(1,(4<<16)+32768,30<<16,14<<16,30<<16,4<<16,38<<16);
|
||||
gr_draw_textured_triangle(4,30,0,0,14,30,3,0,4,38,0,3,0,0,texture_page);
|
||||
gr_set_texture_filter(0);
|
||||
gr_draw_line(3,50,53,50,0x7fff);
|
||||
gr_copy_rect(16,16,2,2,4,4);
|
||||
uint16_t masked[4]={0x7c00,0x7c00,0x7c00,0x7c00};
|
||||
gr_set_mask_bits(0,1); gr_vram_transfer_in(40,40,4,1,masked);
|
||||
gr_set_mask_bits(0,0);
|
||||
gr_fill_rect(1022,510,4,4,0x1234);
|
||||
}
|
||||
static void wait_ready(int st) {
|
||||
const int bounds[4]={0,0,3,3};
|
||||
int ready=0;
|
||||
for(int i=0;i<2000 && !ready;++i) {
|
||||
GpuHdTextureImage image={0};
|
||||
ready=gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,st,&image);
|
||||
gpu_hd_textures_release_image(&image);
|
||||
if(!ready) SDL_Delay(1);
|
||||
}
|
||||
check(ready,"async replacement decode completed");
|
||||
}
|
||||
static void capture(void) {
|
||||
uint32_t argb[64*64];
|
||||
check(gl_renderer_capture_display_hires(argb,64*4,0,0,16,16)==64*64,"high-resolution capture");
|
||||
for(int i=0;i<64*64;++i) {
|
||||
pixels[i*4]=(uint8_t)(argb[i]>>16); pixels[i*4+1]=(uint8_t)(argb[i]>>8);
|
||||
pixels[i*4+2]=(uint8_t)argb[i]; pixels[i*4+3]=(uint8_t)(argb[i]>>24);
|
||||
}
|
||||
}
|
||||
static const uint8_t* sample(int x,int y) { return pixels+(y*64+x)*4; }
|
||||
|
||||
int main(int argc,char** argv) {
|
||||
if(argc==2 && !strcmp(argv[1],"--native-baseline")) {
|
||||
char* native_args[]={"hd-baseline","4","twin","1"};
|
||||
return scale_fixture_main(4,native_args);
|
||||
}
|
||||
if(argc!=2) return 2;
|
||||
for(int i=0;i<16;++i) source_words[i]=i%4==3?0:i%4==2?0x8000:i%4==1?0x83e0:0x03e0;
|
||||
pack_png(argv[1],0); pack_png(argv[1],1);
|
||||
gr_set_backend(GR_BACKEND_SOFTWARE); gr_init(reference);
|
||||
sw_set_faithful_authority(1); native_scene(); sw_set_faithful_authority(0);
|
||||
if(SDL_Init(SDL_INIT_VIDEO)!=0) return 2;
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION,3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
SDL_Window* win=SDL_CreateWindow("HD pack regression",0,0,128,128,SDL_WINDOW_OPENGL|SDL_WINDOW_HIDDEN);
|
||||
if(!win) return 2;
|
||||
gr_set_backend(GR_BACKEND_OPENGL); gr_init(vram); gr_set_scale(4);
|
||||
char error[512]={0};
|
||||
check(gpu_hd_textures_configure(argv[1],1,1,error,sizeof(error)),error);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if(!gl_renderer_init_context(win)) return 2;
|
||||
if(s_hiw) check(hiw_ensure(0,64)!=NULL,"high-resolution window allocated before draws");
|
||||
state(); gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
wait_ready(0); wait_ready(1);
|
||||
check(gpu_hd_textures_reload(error,sizeof(error)),"live pack reload succeeds");
|
||||
/* No new upload: a reload must retain identities of resident textures. */
|
||||
wait_ready(0); wait_ready(1);
|
||||
native_scene(); gl_renderer_sync_cpu();
|
||||
check(memcmp(vram,reference,sizeof(vram))==0,"HD native VRAM matches faithful software across all operations");
|
||||
uint16_t readback[64*64];
|
||||
gr_vram_transfer_out(0,0,64,64,readback);
|
||||
int unchanged=1;
|
||||
for(int y=0;y<64;++y) for(int x=0;x<64;++x) unchanged&=readback[y*64+x]==reference[y*1024+x];
|
||||
check(unchanged,"guest GPUREAD stays native with replacements");
|
||||
|
||||
/* Source rectangle is four native texels, replacement is sixteen. The
|
||||
* first native texel contains alternating red and blue HD columns. */
|
||||
state(); gr_fill_rect(0,0,16,16,0x03e0);
|
||||
gr_draw_textured_rect(4,4,4,4,0,0,0,0,texture_page); capture();
|
||||
const uint8_t* a=sample(16,16); const uint8_t* b=sample(17,16);
|
||||
check(a[0]>200 && a[2]<20 && b[2]>200 && b[0]<20,
|
||||
"replacement detail survives inside one native texel at 4x");
|
||||
check(vram[4*1024+4]==0x03e0,"presentation red/blue never enters guest VRAM");
|
||||
check(sample(16,24)[1]>200,"non-ST alpha127 cuts out black");
|
||||
check(sample(17,24)[0]==0 && sample(17,24)[1]==0 && sample(17,24)[2]==0,
|
||||
"non-ST alpha128 retains opaque black");
|
||||
|
||||
/* ST alpha bytes encode PSX classes, including alpha0 with colored RGB.
|
||||
* Mode 1 is additive, so the green destination survives STP texels. */
|
||||
state(); gr_fill_rect(0,0,16,16,0x03e0); gr_set_semi_transparency(1,1);
|
||||
gr_draw_textured_rect(4,4,4,4,0,0,0,0,texture_page); capture();
|
||||
check(sample(16,16)[0]>200 && sample(16,16)[1]>200,"ST colored alpha0 stays semitransparent");
|
||||
check(sample(19,16)[1]>200,"ST alpha242 stays semitransparent");
|
||||
check(sample(17,16)[1]>200 && sample(18,16)[1]>200,"ST alpha127 and alpha128 stay semitransparent");
|
||||
check(sample(20,16)[0]>200 && sample(20,16)[1]<20,"ST alpha243 is opaque");
|
||||
check(sample(21,16)[2]>200 && sample(21,16)[1]<20,"ST alpha255 is opaque");
|
||||
check(sample(16,24)[1]>200,"ST black RGBA zero cuts out");
|
||||
check(sample(20,24)[1]>200,"ST opaque black follows native-zero cutout");
|
||||
check(sample(17,24)[0]==0 && sample(17,24)[1]>200 && sample(17,24)[2]==0,
|
||||
"ST black alpha127 remains occupied semitransparent black");
|
||||
gr_set_semi_transparency(0,0);
|
||||
|
||||
/* A queued draw must consume its source before a later source overwrite.
|
||||
* The next draw falls back because upload identity has been invalidated. */
|
||||
gr_draw_textured_rect(0,0,4,4,0,0,0,0,texture_page);
|
||||
uint16_t overwrite[16]; for(int i=0;i<16;++i) overwrite[i]=0x7c00;
|
||||
gr_vram_transfer_in(512,0,4,4,overwrite);
|
||||
gr_draw_textured_rect(8,0,4,4,0,0,0,0,texture_page); capture();
|
||||
check(sample(0,0)[0]>200,"queued replacement retained before source overwrite");
|
||||
check(sample(32,0)[2]>200 && sample(32,0)[0]<20,"next draw sees new native source");
|
||||
check(vram[8]==0x7c00,"new source word retained natively");
|
||||
state(); gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
gr_draw_textured_rect(513,0,3,1,0,0,0,0,texture_page);
|
||||
gr_vram_transfer_out(512,0,4,1,readback);
|
||||
check(readback[0]==0x03e0 && readback[1]==0x03e0 && readback[2]==0x03e0 && readback[3]==0x03e0,
|
||||
"self-overlap keeps sequential native texture reads");
|
||||
const int bounds[4]={0,0,3,3}; GpuHdTextureImage lease={0};
|
||||
check(!gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"self-overlap invalidates upload only after its draw");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
check(gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"upload is resident before an interrupted A0");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
gr_vram_upload_begin(512,0,4,4);
|
||||
check(!gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"A0 header invalidates identity even if payload is later aborted");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
gl_renderer_restage_vram_after_savestate();
|
||||
check(!gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"savestate restage cannot fabricate original upload identity");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
check(gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"fresh upload restores identity after state load");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
check(!gl_renderer_texture_banks_supported(),"texture banks explicitly refuse HD authority");
|
||||
check(gl_renderer_pass_unavailable()!=PSX_MOD_RENDER_PASS_READY,"passes refuse HD authority");
|
||||
check(gl_renderer_stereo_unavailable()!=PSX_MOD_RENDER_PASS_READY,"stereo refuses HD authority");
|
||||
GpuHdTextureDiag diag; gpu_hd_textures_get_diag(&diag);
|
||||
check(diag.applied_draws>0,"replacement diagnostics record actual GL submissions");
|
||||
/* Preserve the established Beetle numeric-key path with source origin
|
||||
* offset, native cutout and native STP authority. */
|
||||
char beetle_root[2048],beetle_png[2304],hashes[2304];
|
||||
snprintf(beetle_root,sizeof(beetle_root),"%s/beetle",argv[1]);
|
||||
snprintf(hashes,sizeof(hashes),"%s/Hashes.ini",beetle_root);
|
||||
FILE* hf=fopen(hashes,"wb"); if(hf) fclose(hf);
|
||||
snprintf(beetle_png,sizeof(beetle_png),"%s/demo-texture-replacements/%x-0.png",beetle_root,
|
||||
hd_texture_crc32_words_le(source_words,16));
|
||||
uint8_t beetle_rgba[16*16*4];
|
||||
for(int y=0;y<16;++y) for(int x=0;x<16;++x) {
|
||||
uint8_t* at=beetle_rgba+(y*16+x)*4;
|
||||
at[0]=x<4?255:0; at[1]=x>=8?255:0; at[2]=x>=4&&x<8?255:0; at[3]=255;
|
||||
}
|
||||
FILE* bf=fopen(beetle_png,"wb"); check(bf!=NULL,"Beetle PNG open");
|
||||
if(bf){check(png_write_rgba(bf,beetle_rgba,16,16),"Beetle PNG fixture");fclose(bf);}
|
||||
check(gpu_hd_textures_configure(beetle_root,1,0,error,sizeof(error)),"Beetle session opens");
|
||||
state(); gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
wait_ready(0);
|
||||
gr_vram_transfer_in(516,0,4,4,source_words);
|
||||
gr_fill_rect(0,0,16,16,0x03e0);
|
||||
gr_draw_textured_rect(4,4,2,4,5,0,0,0,texture_page); capture();
|
||||
check(sample(16,16)[2]>200 && sample(16,16)[0]<20,"Beetle partial draw keeps upload-relative UV origin");
|
||||
check(vram[4*1024+4]==0x03e0 && vram[517]==0x83e0,
|
||||
"Beetle image preserves native draw RGB and source STP");
|
||||
gr_draw_textured_rect(8,4,4,4,4,0,0,0,texture_page); capture();
|
||||
check(sample(44,16)[1]>200,"Beetle native transparent zero still cuts out replacement");
|
||||
memcpy(reference,vram,sizeof(vram));
|
||||
gl_renderer_set_cpu_auth_dual(1);
|
||||
gpu_hd_textures_shutdown();
|
||||
check(!s_hd_native_authority,"session teardown clears independent authority");
|
||||
check(gl_renderer_cpu_auth_dual(),"HD teardown preserves netplay authority");
|
||||
check(memcmp(reference,vram,sizeof(vram))==0,"teardown cannot read presentation pixels into native VRAM");
|
||||
gl_renderer_set_cpu_auth_dual(0);
|
||||
check(gl_renderer_texture_banks_supported(),"session teardown restores backend capabilities");
|
||||
check(glGetError()==GL_NO_ERROR,"HD GL errors");
|
||||
printf("scale=%d checks=%d failures=%d applied=%llu dumps=%llu\n",gr_scale(),checks,failures,
|
||||
(unsigned long long)diag.applied_draws,(unsigned long long)diag.dumped_textures);
|
||||
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
|
||||
return failures?1:0;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Original source-owned GL readback-coherence regression. No retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gpu_timeline.c"
|
||||
uint64_t psx_cycle_count=0;
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
* only the authoritative surface, never the window's tiles.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
|
||||
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
|
||||
@@ -82,16 +83,13 @@ void psx_ws_dbg_gate_frame_snapshot(void){}
|
||||
void gpu_depth24_upload_span_reset(void){}
|
||||
/* Netplay unsplit view and forward-pass opt-in: off in these fixtures. */
|
||||
int gpu_ws_netplay_local_viewport_width(void){return 0;}
|
||||
int gpu_ws_background_requires_full_composite(void){return 0;}
|
||||
void gpu_timeline_note(uint8_t kind,uint32_t a,uint32_t b){(void)kind;(void)a;(void)b;}
|
||||
int render_pass_netplay_enabled(void){return 0;}
|
||||
/* Guest clock the renderer's stereo-pair freshness reads; test stand-ins, as
|
||||
* in test_gl_readback_region.c. */
|
||||
uint64_t psx_cycle_count=0;
|
||||
uint32_t g_psx_vblank_cycles=564480u;
|
||||
/* Widescreen/timeline renderer seams the compiled renderer calls; test
|
||||
* stand-ins, like the other return-0 seams above. */
|
||||
int gpu_ws_background_requires_full_composite(void){return 0;}
|
||||
void gpu_timeline_note(uint8_t kind,uint32_t a,uint32_t b){(void)kind;(void)a;(void)b;}
|
||||
|
||||
static uint16_t vram[1024*512], peek[1024*512];
|
||||
static int si_max_dim(void) { return s_gl_max_dim > 0 ? s_gl_max_dim : 1 << 30; }
|
||||
static int checks, failures;
|
||||
|
||||
@@ -28,6 +28,7 @@ DRIVER_C = r"""
|
||||
#include <stdio.h>
|
||||
|
||||
extern uint64_t s_frame_count;
|
||||
extern int test_upload_begins, test_upload_commits, test_upload_rect[4];
|
||||
uint32_t gpu_get_opcode_count(uint8_t op);
|
||||
uint64_t gpu_get_gp0_count(void);
|
||||
void gpu_get_gp0_stats(uint64_t* nop, uint64_t* fill, uint64_t* draw,
|
||||
@@ -184,6 +185,28 @@ int main(int argc, char **argv) {
|
||||
copy_dumped > 0 ? copy_entries[0].csp : 0u,
|
||||
poly_dumped, poly_dumped > 0 ? poly_entries[0].opcode : 0u,
|
||||
poly_dumped > 0 ? poly_entries[0].cmd[0] : 0u);
|
||||
/* Native upload identity is invalidated at the header, even when GP1
|
||||
* aborts a partially written payload. Mask rules stay in gpu.c and bulk
|
||||
* commits carry only the final native words. */
|
||||
gpu_write_gp1(0x00000000u);
|
||||
int begins = test_upload_begins, commits = test_upload_commits;
|
||||
gpu_write_gp0(0xA0000000u); gpu_write_gp0((511u<<16)|1023u);
|
||||
gpu_write_gp0((2u<<16)|4u);
|
||||
if (test_upload_begins != begins+1 || test_upload_rect[0]!=1023 ||
|
||||
test_upload_rect[1]!=511 || test_upload_rect[2]!=4 || test_upload_rect[3]!=2) return 3;
|
||||
gpu_write_gp0(0x33332222u);
|
||||
gpu_write_gp1(0x01000000u);
|
||||
if (test_upload_commits!=commits || gpu_vram_peek(1023,511)!=0x2222u ||
|
||||
gpu_vram_peek(0,511)!=0x3333u) return 4;
|
||||
gpu_write_gp0(0xE6000001u);
|
||||
gpu_write_gp0(0xA0000000u); gpu_write_gp0((100u<<16)|32u);
|
||||
gpu_write_gp0((1u<<16)|2u); gpu_write_gp0(0x00020001u);
|
||||
if (gpu_vram_peek(32,100)!=0x8001u || gpu_vram_peek(33,100)!=0x8002u) return 5;
|
||||
gpu_write_gp0(0xE6000002u);
|
||||
gpu_write_gp0(0xA0000000u); gpu_write_gp0((100u<<16)|32u);
|
||||
gpu_write_gp0((1u<<16)|2u); gpu_write_gp0(0x12341234u);
|
||||
if (gpu_vram_peek(32,100)!=0x8001u || gpu_vram_peek(33,100)!=0x8002u ||
|
||||
test_upload_commits!=commits+2 || test_upload_begins!=begins+3) return 6;
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
@@ -191,6 +214,7 @@ int main(int argc, char **argv) {
|
||||
STUBS_C = r"""
|
||||
#include "gpu.h"
|
||||
#include "gpu_render.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "cpu_state.h"
|
||||
#include "pgxp.h"
|
||||
#include "color_lut.h"
|
||||
@@ -250,6 +274,23 @@ void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
|
||||
int32_t ws_ui_anchor_for_bounds(int32_t x, int32_t width, int32_t display_width)
|
||||
{ (void)x; (void)width; return display_width / 2; }
|
||||
int gte_geometry_correction_enabled(void) { return 0; }
|
||||
/* The fixture is software-only, with native-wide/PGXP disabled. Unexpected
|
||||
* geometry/projective paths remain fatal rather than becoming silent stubs. */
|
||||
int gte_nclip_native_wide_sign(int32_t mac0, int *sign)
|
||||
{ (void)mac0; (void)sign; abort(); }
|
||||
int gte_nclip_native_wide_previous_sign(int32_t mac0, int *sign)
|
||||
{ (void)mac0; (void)sign; abort(); }
|
||||
int pgxp_projection_tracking(void) { return 0; }
|
||||
void pgxp_set_projection_tracking(int enabled)
|
||||
{ if (enabled) abort(); }
|
||||
int pgxp_load_projection(uint32_t addr, uint32_t packed, PGXPProjection *out)
|
||||
{ (void)addr; (void)packed; (void)out; abort(); }
|
||||
int gl_renderer_projective_supported(void) { abort(); }
|
||||
void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3],
|
||||
uint16_t texpage, uint16_t cx, uint16_t cy, int raw, int semi, int perspective)
|
||||
{ (void)vertices; (void)texpage; (void)cx; (void)cy; (void)raw; (void)semi; (void)perspective; abort(); }
|
||||
void gl_renderer_note_wide_triangle_recovery(int enabled)
|
||||
{ if (enabled) abort(); }
|
||||
void pgxp_set_enabled(int enabled) { (void)enabled; }
|
||||
int pgxp_get_gte_sxy_checked(uint32_t slot, uint32_t packed, int require_valid,
|
||||
int32_t *x16, int32_t *y16)
|
||||
@@ -308,9 +349,15 @@ int ws_active(void) { return 0; }
|
||||
int ws_engaged(void) { return 0; }
|
||||
|
||||
void gr_init(uint16_t *vram) { g_vram = vram; }
|
||||
int test_upload_begins, test_upload_commits, test_upload_rect[4];
|
||||
void gr_vram_upload_begin(int x,int y,int w,int h) {
|
||||
++test_upload_begins; test_upload_rect[0]=x; test_upload_rect[1]=y;
|
||||
test_upload_rect[2]=w; test_upload_rect[3]=h;
|
||||
}
|
||||
uint16_t gr_vram_read(int x, int y) { return g_vram[(y & 511) * 1024 + (x & 1023)]; }
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *pixels)
|
||||
{
|
||||
++test_upload_commits;
|
||||
for (int yy = 0; yy < h; yy++)
|
||||
for (int xx = 0; xx < w; xx++)
|
||||
g_vram[((y + yy) & 511) * 1024 + ((x + xx) & 1023)] = pixels[yy * w + xx];
|
||||
|
||||
@@ -29,6 +29,7 @@ static int gp0_state;
|
||||
enum {GP0_VRAM_WRITE=2};
|
||||
static struct {uint32_t gpr[32];} *debug_cpu_ptr;
|
||||
uint32_t psx_read_word(uint32_t addr){(void)addr;assert(0);return 0;}
|
||||
void gr_vram_upload_begin(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
|
||||
"""+production+'\nstatic void capture(uint32_t val) {\n'+capture+'}\n'+r"""
|
||||
int main(void) {
|
||||
gp0_cmd_buf[1]=7|(9u<<16);gp0_cmd_buf[2]=4|(1u<<16);
|
||||
|
||||
@@ -61,6 +61,20 @@ enum { A1 = 5, CAUSE = 13 };
|
||||
#define CAUSE_SYSCALL 0x20u /* ExcCode 8 */
|
||||
#define CAUSE_IP2 0x400u
|
||||
|
||||
/* COP0 reads now invalidate a stale PGXP destination shadow. Observe that
|
||||
* seam explicitly; every unrelated runtime seam remains an abort stub. */
|
||||
static unsigned s_pgxp_calls;
|
||||
static uint32_t s_pgxp_instr, s_pgxp_result;
|
||||
void psx_pgxp_alu(CPUState *cpu, uint32_t instr, uint32_t result,
|
||||
uint32_t s1, uint32_t s2) {
|
||||
++s_pgxp_calls;
|
||||
s_pgxp_instr = instr;
|
||||
s_pgxp_result = result;
|
||||
CHECK(cpu == s_device_cpu, "PGXP received the wrong CPU");
|
||||
CHECK(result == cpu->gpr[A1], "PGXP did not receive the written GPR");
|
||||
CHECK(s1 == 0u && s2 == 0u, "COP0 read inherited ALU source provenance");
|
||||
}
|
||||
|
||||
/* Runs `insn` (MFC0/CFC0 a1,$13) once and returns what it read. The device
|
||||
* deadline falls on the instruction's own cycle, or never. */
|
||||
static uint32_t read_cause(uint32_t insn, int due_on_own_cycle, int raised_before) {
|
||||
@@ -72,6 +86,7 @@ static uint32_t read_cause(uint32_t insn, int due_on_own_cycle, int raised_befor
|
||||
cpu.cop0[CAUSE] = CAUSE_SYSCALL | (raised_before ? CAUSE_IP2 : 0u);
|
||||
s_device_cpu = &cpu;
|
||||
s_services = 0;
|
||||
s_pgxp_calls = 0;
|
||||
psx_cycle_count = 100000u;
|
||||
psx_next_service_cycle = due_on_own_cycle ? psx_cycle_count + 1u : UINT64_MAX;
|
||||
|
||||
@@ -86,6 +101,10 @@ static uint32_t read_cause(uint32_t insn, int due_on_own_cycle, int raised_befor
|
||||
CHECK(cpu.cop0[CAUSE] == cause_after,
|
||||
"0x%08X: CAUSE after the instruction is 0x%08X", (unsigned)insn,
|
||||
(unsigned)cpu.cop0[CAUSE]);
|
||||
CHECK(s_pgxp_calls == 1u && s_pgxp_instr == insn &&
|
||||
s_pgxp_result == cpu.gpr[A1],
|
||||
"0x%08X: missing/duplicate/wrong COP0 destination invalidation",
|
||||
(unsigned)insn);
|
||||
return cpu.gpr[A1];
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@ int render_pass_store_to(const RenderPassStoreTarget *t, uint32_t addr,
|
||||
uint32_t value, uint32_t width) {
|
||||
(void)t; (void)addr; (void)value; (void)width; abort();
|
||||
}
|
||||
/* DMA / host stores drop their word's PGXP shadow (pgxp.cpp). */
|
||||
void pgxp_invalidate_word(uint32_t addr) { (void)addr; }
|
||||
|
||||
unsigned g_trace_write_calls = 0;
|
||||
unsigned g_parity_write_calls = 0;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "mod_local_input_policy.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond) do { if (!(cond)) { fprintf(stderr, "FAIL: %s\n", #cond); failures++; } } while (0)
|
||||
|
||||
int main(void) {
|
||||
/* Digital guest pad mode does not gate local presentation input. */
|
||||
CHECK(psx_mod_local_input_available(1, 1, 0, 0));
|
||||
CHECK(!psx_mod_local_input_available(0, 1, 0, 0));
|
||||
CHECK(!psx_mod_local_input_available(1, 0, 0, 0));
|
||||
CHECK(!psx_mod_local_input_available(1, 1, 1, 0));
|
||||
CHECK(!psx_mod_local_input_available(1, 1, 0, 1));
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -89,8 +89,8 @@ assert "".join(helper[call_at:call_end].split()) == (
|
||||
"&g_mod_owned_baseline,&live,g_mod_native_vblank_rate?1:0);"
|
||||
), "the helper must get the persistent baseline and the live native-pacing flag"
|
||||
|
||||
# Before the call: only the copy-in, so nothing (such as clearing the native
|
||||
# pacing flag) can change what the helper sees.
|
||||
# Before the call: clear the session's local mouse accumulator and copy in
|
||||
# the scalars. Neither may change the native-pacing state the helper captures.
|
||||
COPIES = (
|
||||
("video_vsync", "g_video_vsync"),
|
||||
("frame_interpolation", "g_frame_interpolation"),
|
||||
@@ -100,9 +100,11 @@ COPIES = (
|
||||
("frame_period_ms", "g_frame_period_ms"),
|
||||
)
|
||||
before = c_statements(helper[len(HELPER_SIG):call_at])
|
||||
expected = ["PSXModSessionScalars live;"] + [f"live.{f} = {g};" for f, g in COPIES]
|
||||
expected = ["psx_local_mouse_clear();", "PSXModSessionScalars live;"] + [
|
||||
f"live.{f} = {g};" for f, g in COPIES
|
||||
]
|
||||
assert sorted(before) == sorted(expected), \
|
||||
f"only the scalar copy-in may precede the baseline call, got {before}"
|
||||
f"only mouse reset and scalar copy-in may precede the baseline call, got {before}"
|
||||
|
||||
# After the call: the copy-out, each once, then the native-pacing flag cleared.
|
||||
after = helper[call_end:]
|
||||
@@ -202,7 +204,9 @@ assert first_commit < session_start < first_start < first_disc < reboot, \
|
||||
# Soft return (rematch) re-enters below that block via `goto session_reboot`,
|
||||
# so the rematch path runs the same session start after its commit / netplay
|
||||
# clear, and mounts the disc the new plan selects.
|
||||
rematch = MAIN[MAIN.index('"psxrecomp: cannot clear mods for netplay "'):]
|
||||
rematch_start = MAIN.index('"psxrecomp: cannot apply netplay mods "',
|
||||
MAIN.index("soft_return_lobby:"))
|
||||
rematch = MAIN[rematch_start:]
|
||||
rematch = rematch[:rematch.index("goto session_reboot;")]
|
||||
commit = rematch.index("mod_runtime_commit(resolved_disc,")
|
||||
rematch_session = rematch.index("start_mod_session(net_cfg.enabled);")
|
||||
@@ -222,7 +226,6 @@ assert rematch.count(clamp) == 1, "rematch must re-clamp the aspect to 4:3"
|
||||
clamp_at = rematch.index(clamp)
|
||||
assert commit < clamp_at < rematch_session, \
|
||||
"4:3 re-clamp must follow the commit and precede the session start"
|
||||
rematch_start = MAIN.index('"psxrecomp: cannot clear mods for netplay "')
|
||||
assert MAIN.rfind("case 1: g_video_aspect_num = 16; g_video_aspect_den = 9; break;",
|
||||
0, rematch_start) > MAIN.index("soft_return_lobby:"), \
|
||||
"the launcher's aspect must be applied before the rematch re-clamp"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "psx_sha256.h"
|
||||
#include "psx_lobby_client.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
@@ -44,6 +45,12 @@ extern "C" void psx_host_write_word(uint32_t a, uint32_t v) { psx_write_word(a,
|
||||
extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; }
|
||||
extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) { return 0; }
|
||||
extern "C" void psx_ram_reset_size_request(void) {}
|
||||
extern "C" void gpu_hd_textures_shutdown(void) {}
|
||||
extern "C" int gpu_hd_textures_configure(const char*, int, int, char*, size_t) { return 1; }
|
||||
extern "C" void gpu_hd_textures_set_dump_enabled(int) {}
|
||||
extern "C" int gpu_hd_textures_active(void) { return 1; }
|
||||
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) { *out = {}; }
|
||||
extern "C" int gpu_hd_textures_reload(char*, size_t) { return 1; }
|
||||
extern "C" void psx_projection_reset_session(void) {}
|
||||
extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {}
|
||||
extern "C" int psx_ws_x_margin(void) { return 0; }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "psx_lobby_client.h"
|
||||
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "cpu_state.h"
|
||||
|
||||
#include <array>
|
||||
@@ -82,6 +83,36 @@ extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) {
|
||||
return 0;
|
||||
}
|
||||
extern "C" void psx_ram_reset_size_request(void) {}
|
||||
static int hd_shutdown_calls, hd_configure_calls, hd_dump_value;
|
||||
static int hd_replacements_value, hd_reload_calls;
|
||||
static std::string hd_root;
|
||||
extern "C" void gpu_hd_textures_shutdown(void) { ++hd_shutdown_calls; hd_root.clear(); }
|
||||
extern "C" int gpu_hd_textures_configure(const char* root, int replacements, int dump,
|
||||
char*, size_t) {
|
||||
++hd_configure_calls;
|
||||
hd_root = root;
|
||||
hd_replacements_value = replacements;
|
||||
hd_dump_value = dump;
|
||||
return 1;
|
||||
}
|
||||
extern "C" void gpu_hd_textures_set_dump_enabled(int enabled) {
|
||||
hd_dump_value = enabled;
|
||||
}
|
||||
extern "C" int gpu_hd_textures_active(void) {
|
||||
return !hd_root.empty() && (hd_replacements_value || hd_dump_value);
|
||||
}
|
||||
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) {
|
||||
*out = {};
|
||||
out->root = hd_root.c_str();
|
||||
}
|
||||
extern "C" int gpu_hd_textures_reload(char*, size_t) { ++hd_reload_calls; return 1; }
|
||||
static void test_hd_activation(void) {
|
||||
char replacements[16], dump[16];
|
||||
if (!psx_mod_current_option_value("replacements", replacements, sizeof(replacements)) ||
|
||||
!psx_mod_current_option_value("dump", dump, sizeof(dump))) { ++failures; return; }
|
||||
if (!psx_mod_set_hd_texture_pack("pack", std::string(replacements) == "true",
|
||||
std::string(dump) == "true")) ++failures;
|
||||
}
|
||||
extern "C" void psx_projection_reset_session(void) {}
|
||||
extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {}
|
||||
extern "C" int psx_ws_x_margin(void) { return 0; }
|
||||
@@ -1325,6 +1356,71 @@ int main() {
|
||||
check(psx_mod_read_disc_file("S0/LEVEL.NSF", nullptr, 0, &bytes) && bytes == 69400576u,
|
||||
"files grown past 64 MiB keep their effective size");
|
||||
}
|
||||
{
|
||||
const fs::path hd_mods = root / "hd-mods";
|
||||
write_text(hd_mods / "bundled" / "runtime.hd" / "1.0.0" / "manifest.toml",
|
||||
"format_version = 5\nid = \"runtime.hd\"\nversion = \"1.0.0\"\n"
|
||||
"name = \"HD\"\nresolver = \"declarative\"\nsave_compatibility = \"shared\"\n"
|
||||
"[[target]]\ngame_id = \"SCUS-94236\"\n"
|
||||
"[[feature]]\nid = \"textures\"\nname = \"HD\"\ndefault_enabled = false\n"
|
||||
"[[resource]]\nfeature = \"textures\"\nid = \"pack\"\nlabel = \"Pack\"\n"
|
||||
"format = \"directory\"\nrequired = true\n"
|
||||
"[[option]]\nfeature = \"textures\"\nid = \"replacements\"\n"
|
||||
"label = \"Load\"\ntype = \"boolean\"\ndefault = \"true\"\n"
|
||||
"[[option]]\nfeature = \"textures\"\nid = \"dump\"\n"
|
||||
"label = \"Dump\"\ntype = \"boolean\"\ndefault = \"false\"\n"
|
||||
"[[plugin]]\nfeature = \"textures\"\nid = \"psx.hd-textures\"\n");
|
||||
write_text(hd_mods / "state.toml",
|
||||
"format_version = 2\n[[feature]]\npackage_id = \"runtime.hd\"\n"
|
||||
"id = \"textures\"\nenabled = true\n[feature.values]\ndump = \"true\"\n");
|
||||
check(psx_mod_register_activation_plugin("psx.hd-textures", test_hd_activation),
|
||||
"HD activation plugin registers");
|
||||
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error),
|
||||
"HD mods initialize");
|
||||
const fs::path expected = fs::absolute(hd_mods / "texture-packs" / "SCUS-94236");
|
||||
check(fs::is_directory(expected / "dumps") && fs::is_directory(expected / "replacements"),
|
||||
"default HD directories exist before first launch, even without dumping");
|
||||
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
|
||||
"default pack resource commits without manual folder selection");
|
||||
const int shutdown_before = hd_shutdown_calls;
|
||||
mod_runtime_activate_plugins();
|
||||
check(hd_shutdown_calls == shutdown_before + 1 && hd_configure_calls == 1 &&
|
||||
hd_root == expected.string() && hd_replacements_value == 1 && hd_dump_value == 1,
|
||||
"HD activation clears previous session and reads owning committed options");
|
||||
char value[16] = "old";
|
||||
check(!psx_mod_current_option_value("dump", value, sizeof(value)) && value[0] == '\0',
|
||||
"owning option context does not escape the trusted callback");
|
||||
check(!psx_mod_set_hd_texture_pack("pack", 1, 1),
|
||||
"pack configuration requires the trusted resource callback context");
|
||||
hd_replacements_value = 0;
|
||||
check(psx_mod_set_hd_texture_dump(0) && hd_dump_value == 0,
|
||||
"live dump control forwards the requested switch");
|
||||
check(!gpu_hd_textures_active() && psx_mod_set_hd_texture_dump(1) && hd_dump_value == 1,
|
||||
"dump-only session can turn capture back on after turning it off");
|
||||
check(psx_mod_reload_hd_texture_pack() && hd_reload_calls == 1,
|
||||
"pack reload forwards to renderer");
|
||||
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), "HD plan clears");
|
||||
mod_runtime_activate_plugins();
|
||||
check(hd_shutdown_calls == shutdown_before + 2 && hd_configure_calls == 1,
|
||||
"disabled HD feature leaves no previous session pack active");
|
||||
check(!psx_mod_set_hd_texture_dump(1), "live dump control refuses an unconfigured session");
|
||||
const fs::path custom = hd_mods / fs::u8path(u8"chosen pack # % \u65e5\u672c");
|
||||
fs::create_directories(custom);
|
||||
write_text(hd_mods / "state.toml",
|
||||
"format_version = 2\n[[feature]]\npackage_id = \"runtime.hd\"\n"
|
||||
"id = \"textures\"\nenabled = true\n[feature.resources]\npack = \"" +
|
||||
custom.generic_u8string() + "\"\n");
|
||||
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error) &&
|
||||
PSXRecompV4::mod_runtime_commit(stock_path, &error),
|
||||
"chosen Unicode pack path resolves and persists through commit");
|
||||
mod_runtime_activate_plugins();
|
||||
check(hd_root == custom.generic_u8string(), "renderer receives the chosen folder as UTF-8");
|
||||
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error) &&
|
||||
PSXRecompV4::mod_runtime_commit(stock_path, &error),
|
||||
"committed Unicode pack path reloads without corruption");
|
||||
mod_runtime_activate_plugins();
|
||||
check(hd_root == custom.generic_u8string(), "Unicode pack root survives the complete session cycle");
|
||||
}
|
||||
fs::remove_all(root, ec);
|
||||
if (failures) return 1;
|
||||
std::cout << "mod runtime tests passed\n";
|
||||
|
||||
@@ -132,6 +132,25 @@ int main(void) {
|
||||
CHECK(x == (161 << 16) && y == (81 << 16) && z == 0);
|
||||
}
|
||||
|
||||
/* --- DMA/host rewrite of the identical word: equal bits are not the
|
||||
* same projection, so memory.c drops the shadow by provenance; a
|
||||
* render pass rolls the drop back with the rest of its writes --- */
|
||||
produce_at(ADDR_A);
|
||||
pgxp_invalidate_word(0xA0100000u); /* a mirror of ADDR_A */
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
produce_at(ADDR_A);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
pgxp_checkpoint_begin();
|
||||
pgxp_invalidate_word(ADDR_A);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
pgxp_checkpoint_rollback();
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
pgxp_invalidate_word(0x1F000000u); /* untracked: no slot */
|
||||
|
||||
/* --- LW/SW roundtrip: packet copied by the CPU keeps provenance --- */
|
||||
produce_at(ADDR_A);
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
@@ -148,6 +167,24 @@ int main(void) {
|
||||
CHECK(lookup(ADDR_B, 0xDEADBEEFu, 0, 0, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
|
||||
/* --- coprocessor reads replace the GPR: a word equal to the vertex the
|
||||
* register held is still not that vertex (MFC0 decodes through the ALU
|
||||
* hook, CFC2 through the COP2 hook) --- */
|
||||
{
|
||||
const uint32_t mfc0 = (0x10u << 26) | (8u << 16) | (12u << 11);
|
||||
produce_at(ADDR_A);
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
psx_pgxp_alu(nullptr, mfc0, PACKED, 0, 0);
|
||||
psx_pgxp_store(nullptr, SW(1, 8), ADDR_B, PACKED);
|
||||
CHECK(lookup(ADDR_B, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
psx_pgxp_cop2(nullptr, enc_cop2(0x02, 8, 31), PACKED, 0); /* CFC2 */
|
||||
psx_pgxp_store(nullptr, SW(1, 8), ADDR_B, PACKED);
|
||||
CHECK(lookup(ADDR_B, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
}
|
||||
|
||||
/* --- MOVE idiom (memory mode, no cpu_mode needed) --- */
|
||||
produce_at(ADDR_A);
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
@@ -559,7 +596,14 @@ int main(void) {
|
||||
CHECK(pgxp_probe_precise_vertex(ADDR_A, PACKED, 160, 80) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
CHECK(pgxp_probe_precise_vertex(ADDR_A, PACKED, 161, 80) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
PGXP_SRC_NATIVE); /* truncation reject */
|
||||
CHECK(pgxp_probe_precise_vertex(ADDR_A, PACKED ^ 1u, 160, 80) ==
|
||||
PGXP_SRC_NATIVE); /* value mismatch */
|
||||
const float tolerance_was = pgxp_tolerance();
|
||||
pgxp_set_tolerance(0.1f);
|
||||
CHECK(pgxp_probe_precise_vertex(ADDR_A, PACKED, 160, 80) ==
|
||||
PGXP_SRC_NATIVE); /* tolerance reject */
|
||||
pgxp_set_tolerance(tolerance_was);
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(std::memcmp(&a, &b, sizeof a) == 0);
|
||||
pgxp_note_rect_bypass(1);
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/* DMA and host stores against the PGXP shadow, end to end through the REAL
|
||||
* memory.c store paths and the REAL pgxp.cpp engine.
|
||||
*
|
||||
* A CPU store carries or resets its word's shadow through the PGXP_STORE
|
||||
* hook the emitter and the interpreters place after it. A DMA or host store
|
||||
* (psx_host_write_*) has no hook. Validation on read only catches a writer
|
||||
* that changes the word: one that writes the identical word - a GTE packet
|
||||
* re-uploaded by DMA, a host fill - would leave the old shadow believed,
|
||||
* and two projections that round to the same integer X/Y differ in their
|
||||
* fractions and depth. memory.c therefore drops the shadow of every word a
|
||||
* DMA or host store touches, in RAM (and its mirrors) and the scratchpad,
|
||||
* at word, half and byte width.
|
||||
*
|
||||
* Build/run: ctest -R pgxp_untracked_store_test */
|
||||
#include "pgxp.h"
|
||||
#include "pgxp_hooks.h"
|
||||
#include "psx_memory.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
extern void psx_write_word(uint32_t addr, uint32_t val);
|
||||
extern void psx_write_half(uint32_t addr, uint16_t val);
|
||||
extern void psx_write_byte(uint32_t addr, uint8_t val);
|
||||
extern int g_dma_exec_depth;
|
||||
extern int g_host_store_depth;
|
||||
extern void memory_set_sr_ptr(const uint32_t *p);
|
||||
|
||||
/* gte.cpp position cache: off here, so only the dataflow shadow can answer. */
|
||||
int gte_geometry_correction_lookup(uint32_t p, int32_t *x, int32_t *y) {
|
||||
(void)p; (void)x; (void)y; return 0;
|
||||
}
|
||||
int gte_geometry_correction_lookup_probe(uint32_t p, int32_t *x, int32_t *y) {
|
||||
(void)p; (void)x; (void)y; return 0;
|
||||
}
|
||||
|
||||
static int failures;
|
||||
static void check(int cond, const char *label) {
|
||||
if (!cond) { fprintf(stderr, "FAIL: %s\n", label); failures++; }
|
||||
}
|
||||
|
||||
/* x = 160.5, y = 80.25 rounds to this word. */
|
||||
#define PACKED 0x005000A0u
|
||||
#define X16 ((160 << 16) | 0x8000)
|
||||
#define Y16 ((80 << 16) | 0x4000)
|
||||
#define SWC2_14 ((0x3Au << 26) | (1u << 21) | (14u << 16))
|
||||
|
||||
static void produce(uint32_t addr) {
|
||||
pgxp_gte_push_sxy(X16, Y16, 100, PACKED);
|
||||
psx_pgxp_cop2(NULL, SWC2_14, PACKED, addr); /* the store's hook */
|
||||
}
|
||||
|
||||
static int precise(uint32_t addr) {
|
||||
int32_t x, y; uint16_t z;
|
||||
return pgxp_get_precise_vertex(addr, PACKED, 160, 80, &x, &y, &z) ==
|
||||
PGXP_SRC_DATAFLOW && x == X16 && y == Y16;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static const uint32_t addrs[] = { 0x80100000u, 0x1F800100u };
|
||||
static const uint32_t sr = 0;
|
||||
psx_ram_apply_size_request();
|
||||
memory_set_sr_ptr(&sr);
|
||||
pgxp_set_enabled(1);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
|
||||
for (unsigned i = 0; i < 2; ++i) {
|
||||
const uint32_t a = addrs[i];
|
||||
|
||||
/* A CPU store is the hook's business: memory.c leaves the shadow. */
|
||||
psx_write_word(a, PACKED);
|
||||
produce(a);
|
||||
psx_write_word(a, PACKED);
|
||||
check(precise(a), i ? "scratchpad: CPU store keeps the hooked shadow"
|
||||
: "RAM: CPU store keeps the hooked shadow");
|
||||
|
||||
/* The identical word written by DMA is not the projected vertex. */
|
||||
g_dma_exec_depth = 1;
|
||||
psx_write_word(a, PACKED);
|
||||
g_dma_exec_depth = 0;
|
||||
check(!precise(a), i ? "scratchpad: DMA word store drops the shadow"
|
||||
: "RAM: DMA word store drops the shadow");
|
||||
|
||||
produce(a);
|
||||
psx_host_write_word(a, PACKED);
|
||||
check(!precise(a), i ? "scratchpad: host word store drops the shadow"
|
||||
: "RAM: host word store drops the shadow");
|
||||
|
||||
produce(a);
|
||||
psx_host_write_half(a + 2u, (uint16_t)(PACKED >> 16));
|
||||
check(!precise(a), i ? "scratchpad: host half store drops the shadow"
|
||||
: "RAM: host half store drops the shadow");
|
||||
|
||||
produce(a);
|
||||
psx_host_write_byte(a + 3u, (uint8_t)(PACKED >> 24));
|
||||
check(!precise(a), i ? "scratchpad: host byte store drops the shadow"
|
||||
: "RAM: host byte store drops the shadow");
|
||||
check(g_host_store_depth == 0, "host store depth unwinds");
|
||||
}
|
||||
|
||||
/* A mirror address names the same word and the same shadow. */
|
||||
produce(0x80100000u);
|
||||
g_dma_exec_depth = 1;
|
||||
psx_write_word(0x00100000u, PACKED);
|
||||
g_dma_exec_depth = 0;
|
||||
check(!precise(0x80100000u), "DMA store through a mirror drops the shadow");
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "pgxp_untracked_store_test: %d FAILURES\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("pgxp_untracked_store_test: all checks passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -14,11 +14,13 @@ DMA = (ROOT / "src/dma.c").read_text(encoding="utf-8")
|
||||
BOOT_STATE = (ROOT / "src/boot_state.c").read_text(encoding="utf-8")
|
||||
|
||||
# The per-word DMA2 cursor and XA DATA_END pending bit grow the snapshot wire.
|
||||
# Keep the v7 minimum: v8 adds enhancement memory, while vanilla writers still
|
||||
# emit v7. Older files must be rejected before any live state is applied.
|
||||
# Keep the v7 read minimum: v8 adds enhancement memory, and all current v9
|
||||
# writers preserve the BIOS-to-game handoff latch. Older incompatible files
|
||||
# must be rejected before any live state is applied.
|
||||
assert "#define DMA_GPU_LL_WIRE (4u + (10u * 4u))" in DMA
|
||||
assert "#define BOOT_STATE_VERSION 8u" in BOOT_STATE_H
|
||||
assert "psx_mod_memory_snapshot_bytes() ? BOOT_STATE_VERSION : 7u" in BOOT_STATE
|
||||
assert "#define BOOT_STATE_VERSION 9u" in BOOT_STATE_H
|
||||
assert "h.version = BOOT_STATE_VERSION;" in BOOT_STATE
|
||||
assert "h.version >= 9u && !(seen & (1u << BS_SEC_GAME_START))" in BOOT_STATE
|
||||
assert "#define BOOT_STATE_VERSION_MIN_READ 7u" in BOOT_STATE_H
|
||||
assert "Reject\n * them at the header before any section changes the live machine." in BOOT_STATE_H
|
||||
|
||||
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
xxHash Library
|
||||
Copyright (c) 2012-2021 Yann Collet
|
||||
All rights reserved.
|
||||
|
||||
BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Vendored
+7238
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user