Combine controller stack with validated PGXP fixes and current master; regenerate both BIOS backends

This commit is contained in:
Matthew Stanley
2026-10-07 10:01:37 -07:00
62 changed files with 10641 additions and 122 deletions
+10
View File
@@ -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
+186
View File
@@ -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.
+66
View File
@@ -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.
+1
View File
@@ -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
View File
@@ -1 +1 @@
c93ca7b2e8c1c1cdd94e3d28b3a4d77fe84417974776af7cf69b776c123e8d83
b55f31a1f8d7abe4b04c0322dc961731687343f4ea14d3e722acdc0979f5e183
+8
View File
@@ -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
View File
@@ -1 +1 @@
a6c239ec2d16078269fda42c60a8abd4aaad690d3ec2d5c5aa50f1dd64529e5b
eaafcbd6af676339d2b3a8c29b7394b8729325ab0ff6c117261b4207d2619735
+9
View File
@@ -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"
+1
View File
@@ -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);
+9
View File
@@ -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
+13 -1
View File
@@ -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)
+46
View File
@@ -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.
+139
View File
@@ -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
+4
View File
@@ -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. */
+69
View File
@@ -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
+3
View File
@@ -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);
+22
View File
@@ -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.
+4
View File
@@ -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. */
+2 -1
View File
@@ -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
+32
View File
@@ -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.
+4
View File
@@ -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
+52
View File
@@ -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 },
+2
View File
@@ -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;
}
+751
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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;
}
+272
View File
@@ -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
View File
@@ -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; }
+44
View File
@@ -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
View File
@@ -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;
}
+22
View File
@@ -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);
}
+17
View File
@@ -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
+4 -4
View File
@@ -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))
+85 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 */
+1
View File
@@ -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;
+1
View File
@@ -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) {
+14
View File
@@ -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; }
+26 -6
View File
@@ -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;
}
+251
View File
@@ -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
View File
@@ -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;
+3 -5
View File
@@ -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];
+1
View File
@@ -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
View File
@@ -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; }
+96
View File
@@ -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";
+45 -1
View File
@@ -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);
+114
View File
@@ -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
+26
View File
@@ -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.
+7238
View File
File diff suppressed because it is too large Load Diff