mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Add DuckStation-compatible texture pack mod and gameplay dumping
Load modern texpage/texupload PNG replacements independently of DuckStation source. Integrate pack folders and options with mods, preserve native VRAM, and validate GL rendering, format matching, dump safety, and upload invalidation.
This commit is contained in:
@@ -54,6 +54,7 @@ constrains this repository's own terms; the obligation is to carry the notice.
|
||||
| --- | --- | --- | --- |
|
||||
| [toml11](https://github.com/ToruNiina/toml11) by Toru Niina | MIT | recompiler **and** runtime | `recompiler/lib/toml11/LICENSE`, shipped as `runtime/licenses/toml11-NOTICES.txt` |
|
||||
| [stb_image](http://nothings.org/stb) by Sean Barrett | MIT **or** public domain (Unlicense), at your option | runtime | notice in `runtime/third_party/stb_image.h`, shipped as `runtime/licenses/stb_image-NOTICES.txt` |
|
||||
| [xxHash 0.8.3](https://github.com/Cyan4973/xxHash/tree/v0.8.3) by Yann Collet | BSD-2-Clause | runtime texture-pack interoperability | `runtime/third_party/xxhash.LICENSE`, shipped as `runtime/licenses/xxhash-NOTICES.txt` |
|
||||
| [rabbitizer](https://github.com/Decompollaborate/rabbitizer) by Decompollaborate | MIT | recompiler only | `recompiler/lib/rabbitizer/LICENSE` |
|
||||
| [ELFIO](https://github.com/serge1/ELFIO) by Serge Lamikhov-Center | MIT | recompiler only | `recompiler/lib/ELFIO/LICENSE.txt` |
|
||||
| [{fmt}](https://github.com/fmtlib/fmt) by Victor Zverovich | MIT | recompiler only | `recompiler/lib/fmt/LICENSE.rst` |
|
||||
@@ -65,6 +66,15 @@ runtime dependency lands. The recompiler-only entries are developer tooling
|
||||
and are not in the shipped package; if that ever changes, their notices have
|
||||
to ship too.
|
||||
|
||||
The unmodified xxHash header comes directly from upstream tag `v0.8.3`,
|
||||
`xxhash.h` (SHA-256
|
||||
`17973c0dc49d9854ca26caa191f0e12f7a424b68858d9a78de3860d959d85e4b`).
|
||||
Its BSD notice remains in the header and the separate notice files. The
|
||||
DuckStation-compatible filename parser, matching, PNG dumping and cache are
|
||||
independent psxrecomp implementations; no DuckStation implementation is
|
||||
included. Format observations and the upstream reference pin are recorded in
|
||||
[DUCKSTATION_TEXTURE_FORMAT.md](docs/DUCKSTATION_TEXTURE_FORMAT.md).
|
||||
|
||||
## TinyCC (TCC) — toolchain-free overlay compiler shipped to players
|
||||
|
||||
[TinyCC](https://bellard.org/tcc/) by Fabrice Bellard and contributors, licensed
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# DuckStation texture-pack interoperability
|
||||
|
||||
psxrecomp implements a bounded subset of modern DuckStation texture packs.
|
||||
The parser, matcher, cache and dumper are independently written here. The
|
||||
reference version is upstream commit
|
||||
`697599c47a646a6cfcc4d246018adf4904d55001`, observed on 2026-10-06.
|
||||
This pin matters for palette-range and alpha behavior.
|
||||
|
||||
## Provenance
|
||||
|
||||
At the reference pin, DuckStation's
|
||||
[license](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/LICENSE)
|
||||
is CC-BY-NC-ND-4.0; its texture-cache and shader files carry the same SPDX
|
||||
identifier. This project includes no DuckStation implementation, translated
|
||||
implementation, patch, or binary. Its own code retains the project's
|
||||
PolyForm Noncommercial terms. Reading format behavior does not provide a
|
||||
license to incorporate DuckStation's source. Pack artwork has its own rights;
|
||||
users supply it separately.
|
||||
|
||||
The hash implementation comes directly from
|
||||
[xxHash v0.8.3](https://github.com/Cyan4973/xxHash/tree/v0.8.3), under
|
||||
[BSD-2-Clause](https://github.com/Cyan4973/xxHash/blob/v0.8.3/LICENSE).
|
||||
The unmodified `runtime/third_party/xxhash.h` has SHA-256
|
||||
`17973c0dc49d9854ca26caa191f0e12f7a424b68858d9a78de3860d959d85e4b`.
|
||||
Its license ships as `runtime/licenses/xxhash-NOTICES.txt`.
|
||||
The [v0.8.0 release](https://github.com/Cyan4973/xxHash/releases/tag/v0.8.0)
|
||||
made XXH3 output stable across future versions. Thus the upstream v0.8.3
|
||||
implementation supplies the unseeded XXH3 behavior used by DuckStation's
|
||||
vendored v0.8.0 without importing any emulator code.
|
||||
|
||||
## Supported files
|
||||
|
||||
Choose a game/package folder containing `replacements/` and `dumps/`, or choose
|
||||
`replacements/` itself. Replacement PNGs can live in subdirectories. See the
|
||||
[player workflow](HD_TEXTURE_PACKS.md) for dumping, editing and reloading.
|
||||
DuckStation documents its serial-based folders and authoring process in its
|
||||
[texture replacement wiki](https://github.com/stenzek/duckstation/wiki/Texture-Replacement).
|
||||
|
||||
Paletted filename stems follow this grammar:
|
||||
|
||||
```text
|
||||
<kind>-<mode>-<sourceHash>-<paletteHash>-<sourceWords>x<sourceHeight>-<offsetX>-<offsetY>-<width>x<height>-P<min>-<max>
|
||||
kind = texupload | texpage
|
||||
mode = P4 | P8 | STP4 | STP8
|
||||
```
|
||||
|
||||
Direct stems omit the palette fields:
|
||||
|
||||
```text
|
||||
<kind>-<mode>-<sourceHash>-<sourceWords>x<sourceHeight>-<offsetX>-<offsetY>-<width>x<height>
|
||||
mode = C16 | STC16
|
||||
```
|
||||
|
||||
Append `.png`. Hash fields contain exactly 16 hexadecimal digits; either case
|
||||
is accepted. Dimensions, offsets and palette endpoints are decimal. Source
|
||||
width is in native VRAM words. Rectangle width and X offset are expanded
|
||||
texels: four per word for P4, two for P8, one for C16. Height and Y offset are
|
||||
rows. A page's source size is 64x256, 128x256 or 256x256 words respectively.
|
||||
These definitions follow the
|
||||
[upstream filename documentation](https://github.com/stenzek/duckstation/wiki/Texture-Replacement).
|
||||
|
||||
The loader validates nonzero sizes, native VRAM bounds, word-aligned horizontal
|
||||
rectangles, page bounds and palette endpoints. The replacement PNG's actual
|
||||
size controls its independent X/Y scale; integer and equal-axis scale factors
|
||||
are not required. The renderer applies the declared native rectangle's origin
|
||||
and extent when mapping UVs.
|
||||
|
||||
## Hash compatibility
|
||||
|
||||
Both texture and palette identities use unseeded XXH3-64 over packed native
|
||||
PSX words. Words are serialized low byte first. Rectangles concatenate each
|
||||
row's words without stride padding. `texupload` hashes the complete tracked
|
||||
post-write source rectangle; `texpage` hashes its declared subrectangle.
|
||||
Dimensions and palette data do not enter the texture hash.
|
||||
|
||||
A full CLUT hashes 16 P4 or 256 P8 words, including bit 15. A P8 CLUT extending
|
||||
beyond VRAM's right edge hashes only the remaining words, without wrapping.
|
||||
For reduced `Pmin-max` ranges, the reference implementation hashes the **first
|
||||
`max-min+1` palette words**, without advancing by `min`; a range whose declared
|
||||
maximum crosses the edge does not match. It does not revalidate used indices
|
||||
against the declared range during lookup. psxrecomp preserves this behavior
|
||||
for interoperability. Changing a palette entry outside that prefix can leave
|
||||
a replacement matched; use full ranges when authoring new packs here. These
|
||||
are observations of the pinned
|
||||
[texture-cache behavior](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/src/core/gpu_hw_texture_cache.cpp).
|
||||
|
||||
## Alpha conventions
|
||||
|
||||
The decoder preserves raw RGBA bytes. Ordinary P4/P8/C16 replacements treat
|
||||
alpha below 128 as cutout and alpha at least 128 as occupied, including black.
|
||||
ST names encode PSX transparency classification: alpha at most 242 marks
|
||||
STP, while alpha at least 243 marks an opaque texel. Exact RGBA zero is cutout;
|
||||
ST black with alpha at least 243 also becomes PSX transparent zero. Alpha zero
|
||||
with nonzero RGB in an ST image remains a semitransparent texel. Actual blending
|
||||
still uses the primitive's PSX transparency mode. These rules follow the pinned
|
||||
[replacement merge shader](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/src/core/gpu_hw_shadergen.cpp).
|
||||
|
||||
When both ordinary and ST records match, psxrecomp prefers the primitive's
|
||||
convention; if only the other convention exists it can still supply the image.
|
||||
Multiple matching records with the same preferred convention fall back to
|
||||
native rendering. This deterministic policy avoids dependence on directory
|
||||
enumeration or unordered-map order.
|
||||
|
||||
The dumper writes rounded 5-bit-to-8-bit native RGB, full CLUT ranges, alpha
|
||||
zero for native color zero, alpha 255 for occupied ordinary texels, and alpha
|
||||
128 for occupied STP texels in ST dumps. The pinned upstream dump produces
|
||||
143 in that last case because of an incidental mask expression; both values
|
||||
have the same replacement classification. Dumps here are compatible authoring
|
||||
inputs, not a promise of byte-identical upstream dump PNGs. Native conversion
|
||||
was checked against the pinned
|
||||
[GPU helpers](https://github.com/stenzek/duckstation/blob/697599c47a646a6cfcc4d246018adf4904d55001/src/core/gpu_helpers.h).
|
||||
|
||||
## Current compatibility limits
|
||||
|
||||
The module accepts a single replacement covering the complete draw query.
|
||||
It supports ordinary unchanged uploads and nonwrapping page subrectangles.
|
||||
Every touched upload loses its identity after a partial overwrite; it does
|
||||
not reconstruct split or coalesced upload histories. A same-session pack reload
|
||||
can copy intact upload identities into the newly indexed pack. Loading a
|
||||
savestate clears them; page matching remains available, while upload matching
|
||||
needs subsequent guest uploads. The module's rectangle and query caches must
|
||||
be invalidated on every native VRAM write.
|
||||
|
||||
The following cases use native rendering or report an unsupported-file/option
|
||||
diagnostic:
|
||||
|
||||
- Wrapped UV intervals, horizontally wrapped pages and wrapped uploads.
|
||||
- Draws requiring composition from several replacement images.
|
||||
- `vram-write-` XXH3-128 images, JPEG/WebP payloads and legacy unnamed layouts.
|
||||
- Copy-to-upload conversion, upload coalescing and split-upload identities.
|
||||
- Other `config.yaml` behavior options, including replacement bilinear filtering.
|
||||
|
||||
`config.yaml` supports only a bounded `Aliases:` mapping with literal or quoted
|
||||
filename keys and relative PNG paths. A single-line literal/folded block value
|
||||
is accepted. Escaped strings, multiline values, anchors and general YAML
|
||||
features are outside the subset. Absolute paths and `..` traversal are
|
||||
rejected. A direct canonical replacement takes precedence over its alias.
|
||||
Unknown top-level options are reported by name and are not applied.
|
||||
|
||||
## Bounds and lifecycle
|
||||
|
||||
The loader hard-fails above 262144 candidate files or entries, scans at most
|
||||
1048576 directory entries and limits recursive nesting to 16 levels. Duplicate
|
||||
identities are marked ambiguous. Matching indexes upload hashes and page
|
||||
geometry, with 256 rectangle hashes and 256 positive/negative query results.
|
||||
Native page/CLUT writes invalidate affected cached queries. A query requiring
|
||||
over 4096 candidate page geometries or more than 4 MiB of new hashing reports
|
||||
an error and renders natively.
|
||||
|
||||
One low-priority background worker performs PNG decoding and dumping. The
|
||||
decode queue is bounded to 32, decoded metadata to 512 entries, and decoded
|
||||
pixels to a default 64 MiB budget. Encoded files are limited to 64 MiB and each
|
||||
image dimension to 8192 before decoding. Pixel leases survive eviction and
|
||||
pack destruction. Decoding failure leaves native rendering active.
|
||||
|
||||
Dumps use word-aligned draw rectangles, choose `texupload` when an intact
|
||||
tracked source contains the draw, and otherwise choose `texpage`. The queue
|
||||
holds at most eight snapshots. A bounded set deduplicates 8192 dump identities
|
||||
per session; reaching it reports a diagnostic until dump tracking is reset.
|
||||
PNG writing finishes on pack destruction. A temporary file is published
|
||||
atomically without replacing an existing final file. Write failures remove
|
||||
only the created temporary, print an error even on shutdown, and are returned
|
||||
by the next dump call. All snapshots decode native VRAM rather than replacement
|
||||
pixels.
|
||||
|
||||
## Independent validation
|
||||
|
||||
`runtime/tests/test_duckstation_texture_pack.cpp` contains fixed XXH3 vectors
|
||||
generated with the separate Python xxhash 3.6.0 native wheel. Input word `i`
|
||||
is `((i * 7919) ^ 0xa51c) & 0xffff`, serialized little endian. Examples:
|
||||
|
||||
| Word count | XXH3-64 |
|
||||
| --- | --- |
|
||||
| 0 | `2D06800538D394C2` |
|
||||
| 4 | `3631D2A04EC85311` |
|
||||
| 8 | `000CA76CB9B329DC` |
|
||||
| 16 | `9CBCDEA9305D7528` |
|
||||
| 256 | `0005D0B83A005F4E` |
|
||||
| 8192 | `C68A00DCCA652920` |
|
||||
|
||||
The test also checks literal filenames, strided native rectangles, crop
|
||||
coordinates, CLUT edges and reduced-prefix behavior, positive/negative cache
|
||||
invalidation, reload identity copying, duplicate ambiguity, alias diagnostics,
|
||||
raw alpha thresholds, Unicode folders, background decoding, native dump pixels,
|
||||
worker shutdown and preservation of existing edited files. Renderer fixtures
|
||||
exercise the alpha classification and native-VRAM authority separately.
|
||||
@@ -0,0 +1,60 @@
|
||||
# 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.
|
||||
|
||||
## Install a pack
|
||||
|
||||
1. Open the launcher's **Mods** page and enable **HD Texture Packs**.
|
||||
2. Under **Texture pack folder**, click **Open folder**. On first use the
|
||||
launcher creates `mods/texture-packs/<game-id>` beside the executable, with
|
||||
empty `replacements` and `dumps` directories.
|
||||
3. Copy the pack's replacement PNGs into `replacements`, preserving their
|
||||
filenames. Subdirectories are supported. If the download already contains
|
||||
`replacements`, copy that directory's contents into this one.
|
||||
4. Leave **Load replacements** enabled and **Dump textures** disabled.
|
||||
5. Select **OpenGL** in Settings, then click **Play**.
|
||||
|
||||
**Change folder** selects another pack root. Empty folders are valid; their
|
||||
`replacements` and `dumps` directories are created when the pack is configured.
|
||||
You can also select a pack's `replacements` directory directly, with `dumps`
|
||||
beside it. Keep editable packs outside `mods/bundled`, which builds regenerate.
|
||||
|
||||
## Dump and edit textures
|
||||
|
||||
1. Enable **HD Texture Packs**, disable **Load replacements**, and enable
|
||||
**Dump textures**.
|
||||
2. Click **Play** and visit the scenes you want to capture. Original texture
|
||||
PNGs appear under `dumps` as the game uses them.
|
||||
3. Exit to the launcher. Copy selected PNGs into `replacements`, then edit or
|
||||
enlarge them while keeping the filenames, aspect ratio, and an integer
|
||||
size multiplier.
|
||||
4. Enable **Load replacements**, disable **Dump textures**, and click **Play**
|
||||
using OpenGL to check the artwork.
|
||||
|
||||
The switches apply on **Play**. Restart after adding or editing pack files so
|
||||
the runtime scans them again. Disabling the mod restores the original artwork.
|
||||
The plugin does not write replacement pixels into native VRAM or change the
|
||||
save namespace.
|
||||
|
||||
## Compatibility and diagnostics
|
||||
|
||||
Pack artwork is supplied separately. Matching depends on native game pixels
|
||||
and palettes; a different region or revision may not match. PNG is supported,
|
||||
with a maximum of 8192 pixels per side, 64 MiB encoded, and 64 MiB decoded RGBA.
|
||||
Unsupported identities, configuration features, or images fall back to the
|
||||
original artwork. After loading a savestate, upload-based replacements need
|
||||
fresh texture uploads from the game before they can match again; page-based
|
||||
matching remains available. See [DuckStation texture format notes](DUCKSTATION_TEXTURE_FORMAT.md)
|
||||
for the exact supported filenames, hashing, alpha behavior, and limitations.
|
||||
|
||||
Development builds expose the [TCP `hd_textures` command](TCP_COMMANDS.md).
|
||||
It reports the selected root, switches, backend support, pack size, and matched,
|
||||
ready, and applied draw counts. Optional integer fields `replacements: 0|1`,
|
||||
`dump: 0|1`, and `reload: 1` control an already configured pack during a test
|
||||
session. Use `present_shot` to capture the composed OpenGL window; native VRAM
|
||||
screenshots retain the original artwork.
|
||||
@@ -55,6 +55,7 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `screenshot_wide_hires` | ✓ | | `path`, `base_x` | The displayed band of the native-wide surface at internal resolution (`wide_w×S` by `height×S`). `present_shot` is capped at the window; this checks a widescreen + internal-resolution combination at full size |
|
||||
| `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) |
|
||||
| `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates |
|
||||
| `hd_textures` | ✓ | | optional `replacements`, `dump` (0 or 1), `reload` (1) | HD pack root, switches, backend support, replacement count, matched/ready/applied draw counts, and queued dump count. Optional controls update the configured host pack on the emulation thread; no configured root is an error. Reload retains unchanged upload identities. OpenGL displays replacements; software/Vulkan retain original artwork. Pair with `present_shot` for visual evidence and native VRAM probes for architectural data |
|
||||
| `gl_interp` | ✓ | | — | OpenGL frame-rate presenter ([FRAME_RATE.md](FRAME_RATE.md)): enabled/suspended, host and target Hz, swaps, `source` (`vblank`/`flip`), `flip_period`, `captures` (new source frames) and `duplicates` (VBlanks that re-presented the same frame) |
|
||||
| `render_pass_stats` | ✓ | | — | Render passes ([RENDER_PASSES.md](RENDER_PASSES.md)): plans, phases wanted/planned (shedding), passes, rollbacks (`nesting_repairs`: watchdog aborts whose skipped frame exits the restore undid), dropped device stores by class, `verify_mismatch` under `PSX_RENDER_PASS_VERIFY=1`, host-time split per pass, smoothed pass cost (`cost_us`; `cost_rewarms`: estimates no pass had run on for a while, measured again), presents made from pass images (`late_presents`: held past the frame's planned end because the next flip was late; `expired`: frames whose images stopped showing after several frame lengths without a flip), pass image textures allocated (`image_textures`, `image_bytes`), `status` (`psx_mod_render_pass_status`: 0 ready, 1 no presenter, 2 backend, 3 disabled, 4 session, 5 fast-forward, 6 busy), `backups_reused` (passes that reused the previous pass's VRAM backup), `spans` / `span_failures` and the last failure `span_fail` (`psx_mod_run_guest_span`: reason, exit PC, `$ra`, PC after a call that did not return) |
|
||||
| `window_size` | ✓ | | `w`, `h` | Resize the game window as a player would (Fit / adaptive widescreen follow it): window-shaped checks in `--hidden-window` runs. Pair with `present_shot` |
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
format_version = 5
|
||||
id = "psx.enhancement.hd-textures"
|
||||
version = "1.0.0"
|
||||
name = "HD Texture Packs"
|
||||
author = "psxrecomp"
|
||||
description = "Load compatible DuckStation PNG texture packs with OpenGL or dump original textures during gameplay. No artwork is bundled."
|
||||
resolver = "declarative"
|
||||
save_compatibility = "shared"
|
||||
|
||||
[[target]]
|
||||
game_id = "*"
|
||||
|
||||
[[feature]]
|
||||
id = "hd-textures"
|
||||
name = "HD Texture Packs"
|
||||
description = "Disabled by default. Open the pack folder to install PNG artwork in replacements, or enable Dump textures to collect originals in dumps. Options apply on Play. OpenGL displays replacements; other backends retain original artwork."
|
||||
group = "Visual"
|
||||
default_enabled = false
|
||||
|
||||
[[resource]]
|
||||
feature = "hd-textures"
|
||||
id = "pack"
|
||||
label = "Texture pack folder"
|
||||
description = "The launcher creates mods/texture-packs/<game-id> beside the executable on first use. Open folder to add replacement PNGs or read captured originals; Change folder selects another compatible pack or empty directory."
|
||||
format = "directory"
|
||||
required = true
|
||||
|
||||
[[option]]
|
||||
feature = "hd-textures"
|
||||
id = "replacements"
|
||||
label = "Load replacements"
|
||||
description = "Display matching PNG artwork from replacements in OpenGL. Unsupported or missing replacements retain the original artwork."
|
||||
type = "boolean"
|
||||
default = "true"
|
||||
|
||||
[[option]]
|
||||
feature = "hd-textures"
|
||||
id = "dump"
|
||||
label = "Dump textures"
|
||||
description = "Write original texture PNGs under dumps as they are encountered while playing. Disable after collecting the areas you need."
|
||||
type = "boolean"
|
||||
default = "false"
|
||||
|
||||
[[plugin]]
|
||||
feature = "hd-textures"
|
||||
id = "psx.hd-textures"
|
||||
@@ -102,6 +102,7 @@ int gr_render_display_hires(uint32_t *out_pixels, int out_pitch,
|
||||
int disp_x, int disp_y, int disp_w, int disp_h);
|
||||
|
||||
/* VRAM transfers */
|
||||
void gr_vram_upload_begin(int x, int y, int w, int h);
|
||||
void gr_vram_write(int x, int y, uint16_t pixel);
|
||||
uint16_t gr_vram_read(int x, int y);
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data);
|
||||
|
||||
@@ -270,6 +270,9 @@ void psx_mod_set_world_scene_predicate(PSXModWorldScenePredicate predicate);
|
||||
*/
|
||||
int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
const char* option_id, char* out, uint32_t out_size);
|
||||
/* The committed option for the trusted callback's owning package/feature. */
|
||||
int psx_mod_current_option_value(const char* option_id,
|
||||
char* out, uint32_t out_size);
|
||||
/*
|
||||
* Read the committed owner-selected path for a resource declared by the
|
||||
* package feature whose trusted plugin is currently running. Returns 0 when
|
||||
@@ -278,6 +281,12 @@ int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
*/
|
||||
int psx_mod_current_resource_path(const char* resource_id,
|
||||
char* out, uint32_t out_size);
|
||||
/* Configure an HD pack from the callback's selected directory resource.
|
||||
* Activation/emulation-thread only. Replacements require OpenGL. */
|
||||
int psx_mod_set_hd_texture_pack(const char* resource_id,
|
||||
int replacements_enabled, int dump_enabled);
|
||||
int psx_mod_set_hd_texture_dump(int enabled);
|
||||
int psx_mod_reload_hd_texture_pack(void);
|
||||
/* Read-only canonical media verified by the engine for this plugin's owning
|
||||
* package/feature. The pointer lives until the committed plan is replaced or
|
||||
* cleared. Available only during that plugin's callbacks; returns 0 for an
|
||||
|
||||
@@ -201,6 +201,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)
|
||||
@@ -394,6 +403,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)
|
||||
|
||||
# Registered since before this change, but it did NOT LINK on origin/master:
|
||||
# sio.c had grown psx_read_word / psx_get_cycle_count / i_mask /
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#ifndef PSXRECOMP_DUCKSTATION_TEXTURE_PACK_H
|
||||
#define PSXRECOMP_DUCKSTATION_TEXTURE_PACK_H
|
||||
|
||||
#include "hd_texture_pack.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct DuckTexturePack DuckTexturePack;
|
||||
|
||||
enum DuckTextureKind { DUCK_TEXTURE_UPLOAD = 0, DUCK_TEXTURE_PAGE = 1 };
|
||||
|
||||
/* Independent representation of the modern filename wire format. Source
|
||||
* width is in VRAM words; offsets and rectangle sizes are expanded texels.
|
||||
* Hashes are unseeded XXH3-64 over explicitly little-endian native words. */
|
||||
typedef struct DuckTextureKey {
|
||||
uint64_t source_hash;
|
||||
uint64_t palette_hash;
|
||||
uint16_t source_width_words;
|
||||
uint16_t source_height;
|
||||
uint16_t offset_x;
|
||||
uint16_t offset_y;
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t kind;
|
||||
uint8_t depth; /* enum HdTextureDepth */
|
||||
uint8_t semitransparent; /* STP4 / STP8 / STC16 alpha convention */
|
||||
uint8_t palette_min;
|
||||
uint8_t palette_max;
|
||||
} DuckTextureKey;
|
||||
|
||||
typedef struct DuckTexturePackInfo {
|
||||
const char* root;
|
||||
const char* diagnostic; /* unsupported config/formats and malformed names */
|
||||
size_t replacement_count;
|
||||
size_t ambiguous_count;
|
||||
size_t ignored_count;
|
||||
} DuckTexturePackInfo;
|
||||
|
||||
typedef struct DuckTextureMatch {
|
||||
DuckTextureKey key;
|
||||
uint64_t entry_id;
|
||||
const char* replacement_path;
|
||||
/* Origin of replacement rectangle relative to current texture page, in
|
||||
* texels. May be negative for uploads spanning more than one page. */
|
||||
int32_t origin_u;
|
||||
int32_t origin_v;
|
||||
uint16_t source_width;
|
||||
uint16_t source_height;
|
||||
} DuckTextureMatch;
|
||||
|
||||
typedef struct DuckTexturePixels {
|
||||
const uint8_t* rgba;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t stride;
|
||||
void* lease;
|
||||
} DuckTexturePixels;
|
||||
|
||||
/* Accepts a game/package folder containing replacements/ and dumps/, or the
|
||||
* replacements directory itself. Missing folders are valid empty packs, so
|
||||
* a new pack can start by dumping. Recursive PNG scan; no game assets bundled.
|
||||
* Non-PNG payloads, unsupported options and invalid names are diagnosed. */
|
||||
int duck_texture_pack_create(const char* root, DuckTexturePack** out_pack,
|
||||
char* error, size_t error_capacity);
|
||||
void duck_texture_pack_destroy(DuckTexturePack* pack);
|
||||
void duck_texture_pack_get_info(const DuckTexturePack* pack,
|
||||
DuckTexturePackInfo* out_info);
|
||||
|
||||
int duck_texture_parse_name(const char* filename, DuckTextureKey* out_key);
|
||||
int duck_texture_format_name(const DuckTextureKey* key, char* output,
|
||||
size_t capacity); /* stem only, without .png */
|
||||
uint64_t duck_texture_hash_words_le(const uint16_t* words, size_t count);
|
||||
/* Native word rectangle must not wrap. Zero on invalid input (also a valid
|
||||
* hash value); use valid geometry rather than zero as a success indication. */
|
||||
uint64_t duck_texture_hash_rect(const uint16_t* vram, size_t count,
|
||||
uint16_t x, uint16_t y,
|
||||
uint16_t width_words, uint16_t height);
|
||||
|
||||
/* Track post-write native VRAM, after PSX set/check-mask semantics. Wrapped
|
||||
* uploads are invalidated but deliberately not tracked. Copies, coalescing,
|
||||
* and split-upload identities are outside this initial compatibility subset.
|
||||
* Any touched upload is dropped, so partial overwrites fail conservatively. */
|
||||
int duck_texture_pack_track_upload(DuckTexturePack* pack,
|
||||
uint16_t x, uint16_t y,
|
||||
uint16_t width_words, uint16_t height,
|
||||
const uint16_t* vram, size_t count);
|
||||
void duck_texture_pack_invalidate(DuckTexturePack* pack,
|
||||
uint16_t x, uint16_t y,
|
||||
uint16_t width_words, uint16_t height);
|
||||
void duck_texture_pack_reset_tracking(DuckTexturePack* pack);
|
||||
/* Same-session reload only: copy immutable upload identities while the caller
|
||||
* keeps native VRAM unchanged. Lookup, image and dump caches stay independent.
|
||||
* This is not a savestate restoration API. Strong replacement on success. */
|
||||
int duck_texture_pack_copy_tracking(DuckTexturePack* destination,
|
||||
const DuckTexturePack* source);
|
||||
|
||||
/* Only one replacement covering the entire inclusive query is accepted.
|
||||
* UV wrapping, edge-wrapped pages and multi-image composition fall back.
|
||||
* Uses HD_TEXTURE_LOOKUP_* statuses. Query geometry is shared with Beetle. */
|
||||
int duck_texture_pack_match(DuckTexturePack* pack,
|
||||
const HdTextureDrawQuery* query,
|
||||
DuckTextureMatch* out_match);
|
||||
/* Prefer ST names on semitransparent primitives and ordinary names otherwise;
|
||||
* when only the other convention exists it remains usable. The filename's
|
||||
* convention always determines how the renderer interprets its alpha. */
|
||||
int duck_texture_pack_match_draw(DuckTexturePack* pack,
|
||||
const HdTextureDrawQuery* query,
|
||||
int semitransparent, DuckTextureMatch* out_match);
|
||||
|
||||
/* One bounded background worker; no PNG decoding in the render thread.
|
||||
* Pixels retain raw DuckStation alpha bytes. The renderer interprets the
|
||||
* key.semitransparent flag; this is not conventional opacity for ST names.
|
||||
* Release the lease even after pack destruction. */
|
||||
void duck_texture_pack_set_decode_budget(DuckTexturePack* pack, size_t bytes);
|
||||
int duck_texture_pack_request_decode(DuckTexturePack* pack, uint64_t entry_id);
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* pack, uint64_t entry_id,
|
||||
DuckTexturePixels* out_pixels);
|
||||
void duck_texture_pixels_release(DuckTexturePixels* pixels);
|
||||
|
||||
/* Dumps the word-aligned draw rectangle. Uses texupload when exactly one
|
||||
* unchanged tracked upload covers it; otherwise texpage. Full palette range
|
||||
* is emitted; transparent native zero stays zero, native STP uses alpha 128
|
||||
* for ST names. Existing files are never overwritten. Writes native decoded
|
||||
* pixels only, never already-replaced pixels. Returns FOUND for a new dump,
|
||||
* NONE for an existing/skipped dump, ERROR on invalid input or write failure.
|
||||
* FOUND means queued; bounded background writes finish on pack destruction.
|
||||
* Any asynchronous write failure is reported by the next dump call. */
|
||||
int duck_texture_pack_dump_draw(DuckTexturePack* pack,
|
||||
const HdTextureDrawQuery* query,
|
||||
int semitransparent,
|
||||
char* error, size_t error_capacity);
|
||||
void duck_texture_pack_reset_dump(DuckTexturePack* pack);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -25,6 +25,10 @@ int gl_renderer_select_texture_bank(uint16_t id);
|
||||
/* Retain texture indices while sampling current guest CLUTs (fades/animation). */
|
||||
int gl_renderer_select_texture_bank_live_clut(uint16_t id);
|
||||
int gl_renderer_texture_banks_supported(void);
|
||||
/* HD texture presentation uses an independent native SW VRAM authority.
|
||||
* Both entry points drain pending draws before changing or deleting textures. */
|
||||
void gl_renderer_set_hd_texture_mode(int enabled);
|
||||
void gl_renderer_clear_hd_texture_cache(void);
|
||||
|
||||
/* Set the GL swap interval / vsync mode (1=vsync, 0=immediate, -1=adaptive).
|
||||
* Safe before or after context creation; applies live when a context exists. */
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef PSXRECOMP_GPU_HD_TEXTURES_H
|
||||
#define PSXRECOMP_GPU_HD_TEXTURES_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Emulation-thread configuration. Roots are external user-owned directories;
|
||||
* a successful replacement session selects native CPU VRAM authority on GL.
|
||||
* Reconfiguration retains the previous session if opening the new root fails.
|
||||
* Empty root with both options off clears the current session. */
|
||||
int gpu_hd_textures_configure(const char* root, int replacements_enabled,
|
||||
int dump_enabled, char* error, size_t capacity);
|
||||
void gpu_hd_textures_shutdown(void);
|
||||
int gpu_hd_textures_reload(char* error, size_t capacity);
|
||||
void gpu_hd_textures_set_dump_enabled(int enabled);
|
||||
int gpu_hd_textures_replacements_enabled(void);
|
||||
int gpu_hd_textures_dump_enabled(void);
|
||||
int gpu_hd_textures_active(void);
|
||||
typedef struct GpuHdTextureDiag {
|
||||
const char* root;
|
||||
int active, replacements, dump, format;
|
||||
uint64_t draw_queries, matched_draws, ready_draws, applied_draws, dumped_textures;
|
||||
size_t replacement_count;
|
||||
} GpuHdTextureDiag;
|
||||
void gpu_hd_textures_get_diag(GpuHdTextureDiag* out);
|
||||
void gpu_hd_textures_note_applied(void);
|
||||
/* Non-GL backends can observe native draws for dumping; replacements remain
|
||||
* an OpenGL presentation feature. */
|
||||
void gpu_hd_textures_observe_draw(uint16_t texpage, uint16_t clut_x,
|
||||
uint16_t clut_y, const int limits[4],
|
||||
uint32_t texture_window, int semitransparent);
|
||||
|
||||
/* Native VRAM and residency hooks. A full restage resets identity rather than
|
||||
* pretending a savestate is an original texture upload. */
|
||||
void gpu_hd_textures_set_vram(const uint16_t* vram);
|
||||
void gpu_hd_textures_track_upload(int x, int y, int width_words, int height,
|
||||
const uint16_t* words);
|
||||
void gpu_hd_textures_invalidate(int x, int y, int width_words, int height);
|
||||
void gpu_hd_textures_reset_tracking(void);
|
||||
|
||||
typedef struct GpuHdTextureImage {
|
||||
const uint8_t* rgba;
|
||||
uint32_t width, height, stride;
|
||||
uint64_t cache_key;
|
||||
uint64_t generation;
|
||||
int32_t origin_u, origin_v;
|
||||
uint32_t source_width, source_height;
|
||||
/* 1: Beetle (native STP), 2: Duck non-ST, 3: Duck ST alpha encoding. */
|
||||
int alpha_mode;
|
||||
void* lease;
|
||||
} GpuHdTextureImage;
|
||||
|
||||
/* Inclusive page-UV bounds, before native raster writes the destination.
|
||||
* Texture-window masks are applied to the queried footprint. Pending decode,
|
||||
* ambiguity and unsupported geometry all return 0 for native sampling. */
|
||||
int gpu_hd_textures_acquire_draw(uint16_t texpage, uint16_t clut_x,
|
||||
uint16_t clut_y, const int limits[4],
|
||||
uint32_t texture_window, int semitransparent,
|
||||
GpuHdTextureImage* image);
|
||||
void gpu_hd_textures_release_image(GpuHdTextureImage* image);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -102,6 +102,9 @@ int gr_render_display_hires(uint32_t *out_pixels, int out_pitch,
|
||||
int disp_x, int disp_y, int disp_w, int disp_h);
|
||||
|
||||
/* VRAM transfers */
|
||||
/* A0 header invalidation precedes payload writes; an interrupted transfer
|
||||
* cannot leave a prior immutable texture identity resident. */
|
||||
void gr_vram_upload_begin(int x, int y, int w, int h);
|
||||
void gr_vram_write(int x, int y, uint16_t pixel);
|
||||
uint16_t gr_vram_read(int x, int y);
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data);
|
||||
|
||||
@@ -328,6 +328,11 @@ void psx_mod_set_world_scene_predicate(PSXModWorldScenePredicate predicate);
|
||||
*/
|
||||
int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
const char* option_id, char* out, uint32_t out_size);
|
||||
/* Read an option of the package/feature whose trusted callback is running.
|
||||
* Shared framework plugins use this without hard-coding a title package id.
|
||||
* The same committed-plan and buffer rules as psx_mod_option_value apply. */
|
||||
int psx_mod_current_option_value(const char* option_id,
|
||||
char* out, uint32_t out_size);
|
||||
/*
|
||||
* Read the committed owner-selected path for a resource declared by the
|
||||
* package feature whose trusted plugin is currently running. Returns 0 when
|
||||
@@ -336,6 +341,15 @@ int psx_mod_option_value(const char* package_id, const char* feature_id,
|
||||
*/
|
||||
int psx_mod_current_resource_path(const char* resource_id,
|
||||
char* out, uint32_t out_size);
|
||||
/* Configure the host HD texture pack from this callback's directory resource.
|
||||
* Replacements are currently OpenGL-only; dumping also works on software and
|
||||
* Vulkan. Configuration is host state, never guest RAM or savestate data.
|
||||
* Activation/emulation-thread only. Failure is reported to the runtime log. */
|
||||
int psx_mod_set_hd_texture_pack(const char* resource_id,
|
||||
int replacements_enabled, int dump_enabled);
|
||||
/* Emulation-thread controls for an already configured pack. */
|
||||
int psx_mod_set_hd_texture_dump(int enabled);
|
||||
int psx_mod_reload_hd_texture_pack(void);
|
||||
/* Read-only canonical media verified by the engine for this plugin's owning
|
||||
* package/feature. The pointer lives until the committed plan is replaced or
|
||||
* cleared. Available only during that plugin's callbacks; returns 0 for an
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
xxHash Library
|
||||
Copyright (c) 2012-2021 Yann Collet
|
||||
All rights reserved.
|
||||
|
||||
Vendored header: xxHash v0.8.3, xxhash.h
|
||||
Copyright (C) 2012-2023 Yann Collet
|
||||
Source: https://github.com/Cyan4973/xxHash/blob/v0.8.3/xxhash.h
|
||||
SHA-256: 17973c0dc49d9854ca26caa191f0e12f7a424b68858d9a78de3860d959d85e4b
|
||||
The root LICENSE and the header both grant the BSD 2-Clause terms below.
|
||||
|
||||
BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -365,6 +365,9 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_vram_dirty.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_render.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_hd_textures.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/hd_texture_pack.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/duckstation_texture_pack.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_openxr.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/pad_external_input.c
|
||||
@@ -458,6 +461,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_pgxp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_bezel.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_ram.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_hd_textures.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_media.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#endif
|
||||
#include "dma.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "gpu_render.h" /* gr_scale + gr_render_display_hires (screenshot_hires) */
|
||||
#include "present_ring.h"
|
||||
#include "gpu_timeline.h"
|
||||
@@ -9838,6 +9839,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;
|
||||
@@ -15405,6 +15456,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 },
|
||||
|
||||
@@ -0,0 +1,751 @@
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "../third_party/stb_image.h"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "../third_party/xxhash.h"
|
||||
#include "png_write.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
constexpr size_t kWords = 1024u * 512u;
|
||||
constexpr size_t kMaxEntries = 262144;
|
||||
constexpr size_t kMaxUploads = 8192;
|
||||
constexpr size_t kMaxRectCache = 256;
|
||||
constexpr size_t kMaxDumps = 8192;
|
||||
constexpr size_t kMaxDumpQueue = 8;
|
||||
constexpr size_t kMaxDecodeQueue = 32;
|
||||
constexpr size_t kDefaultBudget = 64u * 1024u * 1024u;
|
||||
constexpr size_t kMaxEncoded = 64u * 1024u * 1024u;
|
||||
constexpr uint32_t kMaxImageDimension = 8192;
|
||||
|
||||
void error_text(char* dest, size_t capacity, const std::string& text) {
|
||||
if (dest && capacity) std::snprintf(dest, capacity, "%s", text.c_str());
|
||||
}
|
||||
std::string lower(std::string s) {
|
||||
for (char& c : s) if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
|
||||
return s;
|
||||
}
|
||||
std::string trim(const std::string& s) {
|
||||
const auto a = s.find_first_not_of(" \t\r\n");
|
||||
if (a == std::string::npos) return {};
|
||||
return s.substr(a, s.find_last_not_of(" \t\r\n") - a + 1);
|
||||
}
|
||||
unsigned expansion(unsigned depth) { return depth == 0 ? 4 : depth == 1 ? 2 : 1; }
|
||||
struct Rect { unsigned x, y, w, h; };
|
||||
bool intersects(Rect a, Rect b) {
|
||||
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
||||
}
|
||||
bool contains(Rect a, Rect b) {
|
||||
return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
|
||||
}
|
||||
bool valid_rect(Rect r) { return r.w && r.h && r.x + r.w <= 1024 && r.y + r.h <= 512; }
|
||||
|
||||
template<class T> bool number(const std::string& text, T& value, int base = 10) {
|
||||
if (text.empty()) return false;
|
||||
const auto result = std::from_chars(text.data(), text.data() + text.size(), value, base);
|
||||
return result.ec == std::errc{} && result.ptr == text.data() + text.size();
|
||||
}
|
||||
bool dimensions(const std::string& s, uint16_t& w, uint16_t& h) {
|
||||
const size_t at = s.find('x');
|
||||
return at != std::string::npos && number(s.substr(0, at), w) &&
|
||||
number(s.substr(at + 1), h) && w && h;
|
||||
}
|
||||
bool valid_key(const DuckTextureKey& k) {
|
||||
if (k.kind > DUCK_TEXTURE_PAGE || k.depth > 2 || k.semitransparent > 1 ||
|
||||
!k.source_width_words || k.source_width_words > 1024 || !k.source_height ||
|
||||
k.source_height > 512 || !k.width || !k.height) return false;
|
||||
const unsigned ppw = expansion(k.depth);
|
||||
if (k.offset_x % ppw || k.width % ppw ||
|
||||
unsigned(k.offset_x) + k.width > unsigned(k.source_width_words) * ppw ||
|
||||
unsigned(k.offset_y) + k.height > k.source_height) return false;
|
||||
if (k.kind == DUCK_TEXTURE_PAGE && (k.source_width_words != 256 / ppw ||
|
||||
k.source_height != 256 || unsigned(k.offset_x) + k.width > 256 ||
|
||||
unsigned(k.offset_y) + k.height > 256)) return false;
|
||||
if (k.depth < 2 && (k.palette_min > k.palette_max ||
|
||||
k.palette_max >= (k.depth == 0 ? 16 : 256))) return false;
|
||||
return k.depth < 2 || (!k.palette_hash && !k.palette_min && !k.palette_max);
|
||||
}
|
||||
std::string stem(const DuckTextureKey& k) {
|
||||
char result[192];
|
||||
if (!duck_texture_format_name(&k, result, sizeof(result))) return {};
|
||||
return result;
|
||||
}
|
||||
bool query_rect(const HdTextureDrawQuery* q, Rect& r) {
|
||||
if (!q || !q->vram || q->vram_word_count < kWords || q->depth > 2 ||
|
||||
q->u_first > q->u_last || q->v_first > q->v_last ||
|
||||
q->page_x >= 1024 || q->page_x % 64 || q->page_y > 256 || q->page_y % 256)
|
||||
return false;
|
||||
const unsigned ppw = expansion(q->depth);
|
||||
/* A page that wraps in X is outside the initial safe subset. */
|
||||
if (q->page_x + 256 / ppw > 1024) return false;
|
||||
r = {q->page_x + q->u_first / ppw, unsigned(q->page_y) + q->v_first,
|
||||
unsigned(q->u_last / ppw - q->u_first / ppw + 1),
|
||||
unsigned(q->v_last - q->v_first + 1)};
|
||||
return valid_rect(r);
|
||||
}
|
||||
|
||||
uint64_t palette_hash(const HdTextureDrawQuery& q, unsigned first, unsigned last, bool& valid) {
|
||||
valid = false;
|
||||
if (q.depth == 2) { valid = true; return 0; }
|
||||
const unsigned full = q.depth == 0 ? 16 : 256;
|
||||
if (first > last || last >= full || q.clut_x >= 1024 || q.clut_y >= 512 || q.clut_x % 16)
|
||||
return 0;
|
||||
unsigned count = last - first + 1;
|
||||
if (first == 0 && last == full - 1) count = std::min(full, 1024u - q.clut_x);
|
||||
else if (q.clut_x + last >= 1024) return 0;
|
||||
/* Compatibility with the pinned upstream behavior: range length is used,
|
||||
* but min does not offset the hashed palette pointer. */
|
||||
valid = true;
|
||||
return duck_texture_hash_words_le(q.vram + q.clut_y * 1024 + q.clut_x, count);
|
||||
}
|
||||
|
||||
struct Entry { DuckTextureKey key{}; std::string path; bool ambiguous = false; };
|
||||
struct Upload { Rect rect; uint64_t hash; };
|
||||
struct RectHash { Rect rect; uint64_t hash; const uint16_t* vram; };
|
||||
struct PageGroup {
|
||||
Rect texels;
|
||||
std::unordered_map<uint64_t, std::vector<size_t>> hashes;
|
||||
};
|
||||
struct MatchCache {
|
||||
HdTextureDrawQuery query;
|
||||
int st, status;
|
||||
DuckTextureMatch result;
|
||||
};
|
||||
struct Image { std::vector<uint8_t> rgba; uint32_t w = 0, h = 0; };
|
||||
struct Lease { std::shared_ptr<Image> image; };
|
||||
enum class DecodeStatus { Queued, Loading, Ready, Failed };
|
||||
struct Decode {
|
||||
DecodeStatus status = DecodeStatus::Queued;
|
||||
std::string path;
|
||||
std::shared_ptr<Image> image;
|
||||
uint64_t use = 0;
|
||||
};
|
||||
struct Dump { fs::path path; std::shared_ptr<Image> image; };
|
||||
/* png_write.h's C-compatible CRC table is mutable during initialization.
|
||||
* Different pack workers therefore serialize this TU's calls to the writer. */
|
||||
std::mutex png_writer_mutex;
|
||||
std::atomic<uint64_t> dump_sequence{static_cast<uint64_t>(std::chrono::steady_clock::now().time_since_epoch().count())};
|
||||
|
||||
FILE* create_exclusive(const fs::path& path) {
|
||||
#ifdef _WIN32
|
||||
HANDLE handle = CreateFileW(path.c_str(),GENERIC_WRITE,0,nullptr,CREATE_NEW,FILE_ATTRIBUTE_NORMAL,nullptr);
|
||||
if (handle == INVALID_HANDLE_VALUE) return nullptr;
|
||||
const int fd = _open_osfhandle(reinterpret_cast<intptr_t>(handle),_O_BINARY | _O_WRONLY);
|
||||
if (fd < 0) { CloseHandle(handle); return nullptr; }
|
||||
FILE* file = _fdopen(fd,"wb");
|
||||
if (!file) _close(fd);
|
||||
#else
|
||||
const int fd = ::open(path.c_str(),O_WRONLY | O_CREAT | O_EXCL,0600);
|
||||
if (fd < 0) return nullptr;
|
||||
FILE* file = ::fdopen(fd,"wb");
|
||||
if (!file) ::close(fd);
|
||||
#endif
|
||||
return file;
|
||||
}
|
||||
|
||||
bool publish_exclusive(const fs::path& temp, const fs::path& target) {
|
||||
#ifdef _WIN32
|
||||
if (MoveFileExW(temp.c_str(),target.c_str(),0)) return true;
|
||||
const DWORD code = GetLastError();
|
||||
if (code != ERROR_ALREADY_EXISTS && code != ERROR_FILE_EXISTS) throw std::runtime_error("cannot publish texture dump PNG");
|
||||
#else
|
||||
if (::link(temp.c_str(),target.c_str()) == 0) return true;
|
||||
if (errno != EEXIST) throw std::runtime_error("cannot publish texture dump PNG");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
std::mutex mutex;
|
||||
std::condition_variable wake;
|
||||
std::unordered_map<uint64_t, Decode> decoded;
|
||||
std::deque<uint64_t> decode_queue;
|
||||
std::deque<Dump> dump_queue;
|
||||
std::thread thread;
|
||||
size_t budget = kDefaultBudget, used = 0;
|
||||
uint64_t tick = 0;
|
||||
bool stop = false;
|
||||
std::string dump_error;
|
||||
~Worker() {
|
||||
{ std::lock_guard<std::mutex> lock(mutex); stop = true; }
|
||||
wake.notify_all();
|
||||
if (thread.joinable()) thread.join();
|
||||
}
|
||||
void evict(size_t incoming) {
|
||||
while (used + incoming > budget) {
|
||||
auto candidate = decoded.end();
|
||||
for (auto it = decoded.begin(); it != decoded.end(); ++it)
|
||||
if (it->second.status == DecodeStatus::Ready &&
|
||||
(candidate == decoded.end() || it->second.use < candidate->second.use)) candidate = it;
|
||||
if (candidate == decoded.end()) break;
|
||||
used -= candidate->second.image->rgba.size();
|
||||
decoded.erase(candidate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void worker_main(Worker* w) {
|
||||
#ifdef _WIN32
|
||||
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
|
||||
#endif
|
||||
for (;;) {
|
||||
uint64_t id = 0; std::string path; Dump dump; size_t budget = 0;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(w->mutex);
|
||||
w->wake.wait(lock, [&] { return w->stop || !w->decode_queue.empty() || !w->dump_queue.empty(); });
|
||||
/* Finish queued dumps on shutdown so the final frame is durable. */
|
||||
if (w->stop && w->dump_queue.empty()) return;
|
||||
if (!w->stop && !w->decode_queue.empty()) {
|
||||
id = w->decode_queue.front(); w->decode_queue.pop_front();
|
||||
auto it = w->decoded.find(id);
|
||||
if (it == w->decoded.end()) continue;
|
||||
it->second.status = DecodeStatus::Loading;
|
||||
path = it->second.path; budget = w->budget;
|
||||
} else { dump = std::move(w->dump_queue.front()); w->dump_queue.pop_front(); }
|
||||
}
|
||||
if (id) {
|
||||
std::shared_ptr<Image> image;
|
||||
try {
|
||||
std::ifstream input(fs::u8path(path), std::ios::binary | std::ios::ate);
|
||||
const auto length = input ? input.tellg() : std::streampos(-1);
|
||||
if (length > 0 && static_cast<uint64_t>(length) <= kMaxEncoded) {
|
||||
std::vector<uint8_t> encoded(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
int width = 0, height = 0, channels = 0;
|
||||
constexpr uint8_t png_signature[] = {137,80,78,71,13,10,26,10};
|
||||
if (input.read(reinterpret_cast<char*>(encoded.data()), length) &&
|
||||
encoded.size() >= sizeof(png_signature) &&
|
||||
!std::memcmp(encoded.data(),png_signature,sizeof(png_signature)) &&
|
||||
stbi_info_from_memory(encoded.data(), static_cast<int>(encoded.size()), &width, &height, &channels) &&
|
||||
width > 0 && height > 0 && width <= int(kMaxImageDimension) && height <= int(kMaxImageDimension) &&
|
||||
uint64_t(width) * height * 4 <= budget) {
|
||||
stbi_uc* pixels = stbi_load_from_memory(encoded.data(), static_cast<int>(encoded.size()),
|
||||
&width, &height, &channels, 4);
|
||||
if (pixels) {
|
||||
std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> owned(pixels, &stbi_image_free);
|
||||
image = std::make_shared<Image>(); image->w = width; image->h = height;
|
||||
image->rgba.assign(pixels, pixels + size_t(width) * height * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (...) { image.reset(); }
|
||||
std::lock_guard<std::mutex> lock(w->mutex);
|
||||
auto it = w->decoded.find(id);
|
||||
if (it == w->decoded.end()) continue;
|
||||
if (image) w->evict(image->rgba.size());
|
||||
if (!image || w->used + image->rgba.size() > w->budget) { it->second.status = DecodeStatus::Failed; continue; }
|
||||
w->used += image->rgba.size(); it->second.image = std::move(image);
|
||||
it->second.status = DecodeStatus::Ready; it->second.use = ++w->tick;
|
||||
} else {
|
||||
bool created = false;
|
||||
fs::path temporary = dump.path;
|
||||
temporary += ".partial-" + std::to_string(++dump_sequence);
|
||||
try {
|
||||
std::error_code ec;
|
||||
fs::create_directories(dump.path.parent_path(), ec);
|
||||
if (ec) throw std::runtime_error("cannot create texture dump directory: " + ec.message());
|
||||
if (fs::exists(dump.path,ec) && !ec) continue;
|
||||
FILE* file = create_exclusive(temporary);
|
||||
if (!file) throw std::runtime_error("cannot create temporary texture dump PNG");
|
||||
created = true;
|
||||
bool success = false;
|
||||
try {
|
||||
std::lock_guard<std::mutex> write_lock(png_writer_mutex);
|
||||
success = png_write_rgba(file,dump.image->rgba.data(),dump.image->w,dump.image->h) != 0;
|
||||
} catch (...) { std::fclose(file); throw; }
|
||||
if (std::fclose(file) != 0 || !success) throw std::runtime_error("cannot write texture dump PNG");
|
||||
publish_exclusive(temporary,dump.path);
|
||||
fs::remove(temporary,ec);
|
||||
} catch (const std::exception& e) {
|
||||
if (created) { std::error_code ignored; fs::remove(temporary,ignored); }
|
||||
std::lock_guard<std::mutex> lock(w->mutex); w->dump_error = e.what();
|
||||
std::fprintf(stderr,"[texture-pack] dump failed: %s\n",e.what());
|
||||
} catch (...) {
|
||||
if (created) { std::error_code ignored; fs::remove(temporary,ignored); }
|
||||
std::lock_guard<std::mutex> lock(w->mutex); w->dump_error = "texture dump allocation failed";
|
||||
std::fprintf(stderr,"[texture-pack] dump failed: allocation failed\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct DuckTexturePack {
|
||||
std::string root, diagnostic;
|
||||
fs::path replacements, dumps;
|
||||
std::vector<Entry> entries;
|
||||
std::array<std::unordered_map<uint64_t, std::vector<size_t>>, 3> uploads_by_hash;
|
||||
std::array<std::unordered_map<uint64_t, PageGroup>, 3> page_groups;
|
||||
std::unordered_map<std::string, size_t> by_name;
|
||||
std::vector<Upload> uploads;
|
||||
std::vector<RectHash> rect_cache;
|
||||
std::vector<MatchCache> match_cache;
|
||||
std::unordered_set<std::string> dumped;
|
||||
size_t ignored = 0, ambiguous = 0;
|
||||
bool geometry_limit_reported = false, dump_limit_reported = false;
|
||||
Worker worker;
|
||||
};
|
||||
|
||||
namespace {
|
||||
void note(DuckTexturePack& p, const std::string& s) {
|
||||
++p.ignored;
|
||||
if (p.diagnostic.size() < 2048) p.diagnostic += (p.diagnostic.empty() ? "" : "; ") + s;
|
||||
}
|
||||
void add_entry(DuckTexturePack& p, const DuckTextureKey& key, const fs::path& path) {
|
||||
const std::string name = stem(key);
|
||||
auto previous = p.by_name.find(name);
|
||||
if (previous != p.by_name.end()) {
|
||||
auto& entry = p.entries[previous->second];
|
||||
if (!entry.ambiguous) { entry.ambiguous = true; ++p.ambiguous; }
|
||||
return;
|
||||
}
|
||||
if (p.entries.size() >= kMaxEntries) throw std::runtime_error("texture replacement entry limit (262144) exceeded");
|
||||
p.by_name.emplace(name, p.entries.size());
|
||||
if (key.kind == DUCK_TEXTURE_UPLOAD) p.uploads_by_hash[key.depth][key.source_hash].push_back(p.entries.size());
|
||||
else {
|
||||
const uint64_t geometry = (uint64_t(key.offset_x) << 48) | (uint64_t(key.offset_y) << 32) |
|
||||
(uint64_t(key.width) << 16) | key.height;
|
||||
PageGroup& group = p.page_groups[key.depth][geometry];
|
||||
group.texels = {key.offset_x, key.offset_y, key.width, key.height};
|
||||
group.hashes[key.source_hash].push_back(p.entries.size());
|
||||
}
|
||||
p.entries.push_back({key, path.u8string(), false});
|
||||
}
|
||||
uint64_t cached_hash(DuckTexturePack& p, const HdTextureDrawQuery& q, Rect r) {
|
||||
for (const auto& saved : p.rect_cache)
|
||||
if (saved.vram == q.vram && saved.rect.x == r.x && saved.rect.y == r.y && saved.rect.w == r.w && saved.rect.h == r.h) return saved.hash;
|
||||
const uint64_t value = duck_texture_hash_rect(q.vram, q.vram_word_count, r.x, r.y, r.w, r.h);
|
||||
if (p.rect_cache.size() >= kMaxRectCache) p.rect_cache.erase(p.rect_cache.begin());
|
||||
p.rect_cache.push_back({r, value, q.vram});
|
||||
return value;
|
||||
}
|
||||
std::string unquote(std::string s) {
|
||||
s = trim(s);
|
||||
if (s.size() > 1 && ((s.front() == '\'' && s.back() == '\'') || (s.front() == '"' && s.back() == '"')))
|
||||
return s.substr(1, s.size() - 2);
|
||||
const auto comment = s.find(" #");
|
||||
return comment == std::string::npos ? s : trim(s.substr(0, comment));
|
||||
}
|
||||
void load_config(DuckTexturePack& p, const fs::path& path) {
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec)) return;
|
||||
if (fs::file_size(path, ec) > 1024 * 1024 || ec) { note(p, "config.yaml exceeds 1 MiB"); return; }
|
||||
std::ifstream file(path); std::string line; bool aliases = false;
|
||||
while (std::getline(file, line)) {
|
||||
const std::string text = trim(line);
|
||||
if (text.empty() || text.front() == '#') continue;
|
||||
if (line.size() > 4096) { note(p, "oversized config.yaml line"); continue; }
|
||||
const auto colon = text.find(':');
|
||||
if (colon == std::string::npos) { note(p, "unsupported config.yaml syntax"); continue; }
|
||||
const std::string key = unquote(text.substr(0, colon));
|
||||
std::string value = unquote(text.substr(colon + 1));
|
||||
const bool indented = line.front() == ' ' || line.front() == '\t';
|
||||
if (!indented) { aliases = key == "Aliases"; if (!aliases) note(p, "config.yaml option not applied: " + key); continue; }
|
||||
if (!aliases) continue;
|
||||
DuckTextureKey parsed{};
|
||||
if (!duck_texture_parse_name(key.c_str(), &parsed)) { note(p, "invalid alias key"); continue; }
|
||||
if (value == "|" || value == "|-" || value == ">" || value == ">-") {
|
||||
if (!std::getline(file, line) || line.find_first_not_of(" \t") < 4) { note(p, "unsupported alias block"); continue; }
|
||||
value = trim(line);
|
||||
}
|
||||
fs::path relative = fs::u8path(value);
|
||||
bool unsafe = relative.empty() || relative.is_absolute() || relative.has_root_name();
|
||||
for (const auto& part : relative) if (part == "..") unsafe = true;
|
||||
if (unsafe || lower(relative.extension().string()) != ".png") { note(p, "unsupported alias path"); continue; }
|
||||
const fs::path target = p.replacements / relative;
|
||||
if (!fs::is_regular_file(target, ec) || ec) { note(p, "alias target missing"); ec.clear(); continue; }
|
||||
const auto old = p.by_name.find(stem(parsed));
|
||||
/* A direct canonical filename takes precedence over an alias. */
|
||||
if (old == p.by_name.end()) add_entry(p, parsed, target);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
int duck_texture_parse_name(const char* filename, DuckTextureKey* out) {
|
||||
if (!filename || !out) return 0;
|
||||
try {
|
||||
std::string text(filename);
|
||||
if (text.size() > 192 || text.find_first_of("/\\") != std::string::npos) return 0;
|
||||
const auto dot = text.find('.');
|
||||
if (dot != std::string::npos) {
|
||||
if (lower(text.substr(dot)) != ".png") return 0;
|
||||
text.resize(dot);
|
||||
}
|
||||
std::vector<std::string> parts;
|
||||
size_t begin = 0;
|
||||
for (size_t at = 0; at <= text.size(); ++at) if (at == text.size() || text[at] == '-') {
|
||||
parts.push_back(text.substr(begin, at - begin)); begin = at + 1;
|
||||
}
|
||||
if (parts.size() != 7 && parts.size() != 10) return 0;
|
||||
DuckTextureKey k{};
|
||||
if (parts[0] == "texupload") k.kind = DUCK_TEXTURE_UPLOAD;
|
||||
else if (parts[0] == "texpage") k.kind = DUCK_TEXTURE_PAGE;
|
||||
else return 0;
|
||||
std::string mode = parts[1];
|
||||
if (mode.size() > 2 && mode.substr(0, 2) == "ST") { k.semitransparent = 1; mode.erase(0, 2); }
|
||||
if (mode == "P4") k.depth = 0; else if (mode == "P8") k.depth = 1; else if (mode == "C16") k.depth = 2; else return 0;
|
||||
if (parts[2].size() != 16 || !number(parts[2], k.source_hash, 16)) return 0;
|
||||
size_t field = 3;
|
||||
if (k.depth < 2) {
|
||||
if (parts.size() != 10 || parts[3].size() != 16 || !number(parts[3], k.palette_hash, 16)) return 0;
|
||||
++field;
|
||||
} else if (parts.size() != 7) return 0;
|
||||
if (!dimensions(parts[field++], k.source_width_words, k.source_height) ||
|
||||
!number(parts[field++], k.offset_x) || !number(parts[field++], k.offset_y) ||
|
||||
!dimensions(parts[field++], k.width, k.height)) return 0;
|
||||
if (k.depth < 2) {
|
||||
unsigned a = 0, b = 0;
|
||||
if (parts[field].empty() || parts[field][0] != 'P' || !number(parts[field].substr(1), a) ||
|
||||
!number(parts[field + 1], b) || a > 255 || b > 255) return 0;
|
||||
k.palette_min = static_cast<uint8_t>(a); k.palette_max = static_cast<uint8_t>(b);
|
||||
}
|
||||
if (!valid_key(k)) return 0;
|
||||
*out = k; return 1;
|
||||
} catch (...) { return 0; }
|
||||
}
|
||||
|
||||
int duck_texture_format_name(const DuckTextureKey* k, char* out, size_t capacity) {
|
||||
if (!k || !out || !capacity || !valid_key(*k)) return 0;
|
||||
const char* kind = k->kind == DUCK_TEXTURE_UPLOAD ? "texupload" : "texpage";
|
||||
const char* mode = k->depth == 0 ? "P4" : k->depth == 1 ? "P8" : "C16";
|
||||
const char* st = k->semitransparent ? "ST" : "";
|
||||
int count;
|
||||
if (k->depth < 2) count = std::snprintf(out, capacity,
|
||||
"%s-%s%s-%016llX-%016llX-%ux%u-%u-%u-%ux%u-P%u-%u", kind, st, mode,
|
||||
static_cast<unsigned long long>(k->source_hash), static_cast<unsigned long long>(k->palette_hash),
|
||||
k->source_width_words, k->source_height, k->offset_x, k->offset_y, k->width, k->height, k->palette_min, k->palette_max);
|
||||
else count = std::snprintf(out, capacity, "%s-%s%s-%016llX-%ux%u-%u-%u-%ux%u", kind, st, mode,
|
||||
static_cast<unsigned long long>(k->source_hash), k->source_width_words, k->source_height,
|
||||
k->offset_x, k->offset_y, k->width, k->height);
|
||||
return count >= 0 && static_cast<size_t>(count) < capacity;
|
||||
}
|
||||
|
||||
uint64_t duck_texture_hash_words_le(const uint16_t* words, size_t count) {
|
||||
if ((!words && count) || count > kWords) return 0;
|
||||
XXH3_state_t state;
|
||||
XXH3_64bits_reset(&state);
|
||||
std::array<uint8_t, 2048> bytes{};
|
||||
while (count) {
|
||||
const size_t chunk = std::min(count, bytes.size() / 2);
|
||||
for (size_t i = 0; i < chunk; ++i) { bytes[2 * i] = words[i] & 255; bytes[2 * i + 1] = words[i] >> 8; }
|
||||
XXH3_64bits_update(&state, bytes.data(), chunk * 2);
|
||||
words += chunk; count -= chunk;
|
||||
}
|
||||
return XXH3_64bits_digest(&state);
|
||||
}
|
||||
uint64_t duck_texture_hash_rect(const uint16_t* vram, size_t count, uint16_t x, uint16_t y,
|
||||
uint16_t width, uint16_t height) {
|
||||
if (!vram || count < kWords || !valid_rect({x, y, width, height})) return 0;
|
||||
XXH3_state_t state; XXH3_64bits_reset(&state);
|
||||
std::array<uint8_t, 2048> bytes{};
|
||||
for (unsigned row = 0; row < height; ++row) {
|
||||
const uint16_t* src = vram + (y + row) * 1024 + x;
|
||||
for (unsigned i = 0; i < width; ++i) { bytes[2 * i] = src[i] & 255; bytes[2 * i + 1] = src[i] >> 8; }
|
||||
XXH3_64bits_update(&state, bytes.data(), size_t(width) * 2);
|
||||
}
|
||||
return XXH3_64bits_digest(&state);
|
||||
}
|
||||
|
||||
int duck_texture_pack_create(const char* root, DuckTexturePack** out, char* error, size_t capacity) {
|
||||
if (out) *out = nullptr;
|
||||
if (!out || !root || !root[0]) { error_text(error, capacity, "texture pack root is unset"); return 0; }
|
||||
try {
|
||||
auto p = std::make_unique<DuckTexturePack>();
|
||||
std::error_code ec; fs::path input = fs::absolute(fs::u8path(root), ec).lexically_normal();
|
||||
if (ec || (fs::exists(input, ec) && !fs::is_directory(input, ec))) {
|
||||
error_text(error, capacity, "texture pack root is not a directory"); return 0;
|
||||
}
|
||||
if (lower(input.filename().u8string()) == "replacements") { p->replacements = input; input = input.parent_path(); }
|
||||
else p->replacements = input / "replacements";
|
||||
p->root = input.u8string(); p->dumps = input / "dumps";
|
||||
std::vector<fs::path> paths;
|
||||
if (fs::is_directory(p->replacements, ec)) {
|
||||
size_t scanned = 0;
|
||||
for (fs::recursive_directory_iterator it(p->replacements, fs::directory_options::skip_permission_denied, ec), end;
|
||||
!ec && it != end; it.increment(ec)) {
|
||||
if (++scanned > kMaxEntries * 4) throw std::runtime_error("texture replacement directory scan limit exceeded");
|
||||
if (it.depth() >= 16) { if (it->is_directory(ec)) it.disable_recursion_pending(); note(*p, "replacement nesting exceeds 16 levels"); }
|
||||
if (it->is_regular_file(ec)) {
|
||||
const std::string name = it->path().filename().u8string();
|
||||
if (name.rfind("texupload-", 0) != 0 && name.rfind("texpage-", 0) != 0 && name.rfind("vram-write-", 0) != 0) continue;
|
||||
if (lower(it->path().extension().string()) != ".png") { note(*p, "replacement format outside PNG subset"); continue; }
|
||||
if (paths.size() >= kMaxEntries) throw std::runtime_error("texture replacement file limit (262144) exceeded");
|
||||
paths.push_back(it->path());
|
||||
}
|
||||
}
|
||||
if (ec) { error_text(error, capacity, "cannot scan replacement directory: " + ec.message()); return 0; }
|
||||
} else if (ec && ec != std::errc::no_such_file_or_directory) {
|
||||
error_text(error, capacity, "cannot inspect replacement directory: " + ec.message()); return 0;
|
||||
}
|
||||
std::sort(paths.begin(), paths.end());
|
||||
for (const auto& path : paths) {
|
||||
DuckTextureKey k{};
|
||||
if (duck_texture_parse_name(path.filename().u8string().c_str(), &k)) add_entry(*p, k, path);
|
||||
else note(*p, "invalid or unsupported replacement filename");
|
||||
}
|
||||
load_config(*p, input / "config.yaml");
|
||||
*out = p.release(); return 1;
|
||||
} catch (const std::exception& e) { error_text(error, capacity, e.what()); return 0; }
|
||||
catch (...) { error_text(error, capacity, "texture pack allocation failed"); return 0; }
|
||||
}
|
||||
void duck_texture_pack_destroy(DuckTexturePack* p) { delete p; }
|
||||
void duck_texture_pack_get_info(const DuckTexturePack* p, DuckTexturePackInfo* out) {
|
||||
if (!out) return;
|
||||
*out = {};
|
||||
if (p) { out->root = p->root.c_str(); out->diagnostic = p->diagnostic.c_str(); out->replacement_count = p->entries.size();
|
||||
out->ambiguous_count = p->ambiguous; out->ignored_count = p->ignored; }
|
||||
}
|
||||
void duck_texture_pack_invalidate(DuckTexturePack* p, uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||
if (!p || !w || !h) return;
|
||||
if (w > 1024 || h > 512) { duck_texture_pack_reset_tracking(p); return; }
|
||||
const unsigned px = x % 1024, py = y % 512;
|
||||
const unsigned w0 = std::min(unsigned(w), 1024 - px), h0 = std::min(unsigned(h), 512 - py);
|
||||
const std::array<Rect, 4> regions{{{px,py,w0,h0},{0,py,unsigned(w)-w0,h0},
|
||||
{px,0,w0,unsigned(h)-h0},{0,0,unsigned(w)-w0,unsigned(h)-h0}}};
|
||||
for (const auto& r : regions) if (r.w && r.h) {
|
||||
const size_t old_uploads = p->uploads.size();
|
||||
p->uploads.erase(std::remove_if(p->uploads.begin(), p->uploads.end(), [&](const Upload& u){ return intersects(u.rect,r); }), p->uploads.end());
|
||||
p->rect_cache.erase(std::remove_if(p->rect_cache.begin(), p->rect_cache.end(), [&](const RectHash& v){ return intersects(v.rect,r); }), p->rect_cache.end());
|
||||
if (p->uploads.size() != old_uploads) p->match_cache.clear();
|
||||
else p->match_cache.erase(std::remove_if(p->match_cache.begin(), p->match_cache.end(), [&](const MatchCache& c) {
|
||||
const auto& q = c.query;
|
||||
const Rect page{q.page_x,q.page_y,256 / expansion(q.depth),256};
|
||||
const Rect clut{q.clut_x,q.clut_y,std::min(q.depth == 0 ? 16u : 256u,1024u-q.clut_x),1};
|
||||
return intersects(page,r) || (q.depth < 2 && intersects(clut,r));
|
||||
}), p->match_cache.end());
|
||||
}
|
||||
}
|
||||
void duck_texture_pack_reset_tracking(DuckTexturePack* p) { if (p) { p->uploads.clear(); p->rect_cache.clear(); p->match_cache.clear(); } }
|
||||
int duck_texture_pack_copy_tracking(DuckTexturePack* destination, const DuckTexturePack* source) {
|
||||
if (!destination || !source) return 0;
|
||||
try {
|
||||
auto uploads = source->uploads;
|
||||
destination->uploads.swap(uploads);
|
||||
destination->rect_cache.clear();
|
||||
destination->match_cache.clear();
|
||||
return 1;
|
||||
} catch (...) { return 0; }
|
||||
}
|
||||
int duck_texture_pack_track_upload(DuckTexturePack* p, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
const uint16_t* vram, size_t count) {
|
||||
if (!p) return 0;
|
||||
duck_texture_pack_invalidate(p, x, y, w, h);
|
||||
if (!vram || count < kWords || !valid_rect({x,y,w,h})) return 0;
|
||||
try {
|
||||
if (p->uploads.size() >= kMaxUploads) p->uploads.erase(p->uploads.begin());
|
||||
p->uploads.push_back({{x,y,w,h},duck_texture_hash_rect(vram,count,x,y,w,h)});
|
||||
p->match_cache.clear(); return 1;
|
||||
} catch (...) { return 0; }
|
||||
}
|
||||
|
||||
int duck_texture_pack_match(DuckTexturePack* p, const HdTextureDrawQuery* q, DuckTextureMatch* out) {
|
||||
return duck_texture_pack_match_draw(p,q,0,out);
|
||||
}
|
||||
int duck_texture_pack_match_draw(DuckTexturePack* p, const HdTextureDrawQuery* q, int st, DuckTextureMatch* out) {
|
||||
if (out) *out = {};
|
||||
if (!p || !q || !out) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
Rect wanted{}; if (!query_rect(q, wanted)) return HD_TEXTURE_LOOKUP_NONE;
|
||||
try {
|
||||
for (const auto& c : p->match_cache) {
|
||||
const auto& a = c.query;
|
||||
if (c.st == (st != 0) && a.vram == q->vram && a.page_x == q->page_x && a.page_y == q->page_y &&
|
||||
a.depth == q->depth && a.clut_x == q->clut_x && a.clut_y == q->clut_y &&
|
||||
a.u_first == q->u_first && a.u_last == q->u_last && a.v_first == q->v_first && a.v_last == q->v_last) {
|
||||
*out = c.result; return c.status;
|
||||
}
|
||||
}
|
||||
const unsigned ppw = expansion(q->depth);
|
||||
size_t winner = size_t(-1); int origin_u = 0, origin_v = 0, rank = 2;
|
||||
bool ambiguous = false;
|
||||
const auto consider = [&](size_t index, int u, int v) {
|
||||
const Entry& e = p->entries[index]; const auto& k = e.key;
|
||||
const int candidate_rank = k.semitransparent == (st != 0) ? 0 : 1;
|
||||
if (candidate_rank > rank) return;
|
||||
bool valid = false;
|
||||
if (palette_hash(*q,k.palette_min,k.palette_max,valid) != k.palette_hash || !valid) return;
|
||||
if (candidate_rank < rank) { rank = candidate_rank; winner = size_t(-1); ambiguous = false; }
|
||||
if (e.ambiguous || winner != size_t(-1)) ambiguous = true;
|
||||
winner = index; origin_u = u; origin_v = v;
|
||||
};
|
||||
for (const auto& upload : p->uploads) {
|
||||
if (!contains(upload.rect,wanted)) continue;
|
||||
const auto found = p->uploads_by_hash[q->depth].find(upload.hash);
|
||||
if (found == p->uploads_by_hash[q->depth].end()) continue;
|
||||
for (const size_t index : found->second) {
|
||||
const auto& k = p->entries[index].key;
|
||||
if (upload.rect.w != k.source_width_words || upload.rect.h != k.source_height) continue;
|
||||
Rect rect{upload.rect.x+k.offset_x/ppw,upload.rect.y+k.offset_y,unsigned(k.width)/ppw,k.height};
|
||||
if (contains(rect,wanted)) consider(index,(int(upload.rect.x)-q->page_x)*int(ppw)+k.offset_x,
|
||||
int(upload.rect.y)-q->page_y+k.offset_y);
|
||||
}
|
||||
}
|
||||
size_t groups_checked = 0, hashed_bytes = 0; bool over_budget = false;
|
||||
for (const auto& item : p->page_groups[q->depth]) {
|
||||
const auto& group = item.second; const auto& r = group.texels;
|
||||
Rect rect{q->page_x+r.x/ppw,q->page_y+r.y,r.w/ppw,r.h};
|
||||
if (!contains(rect,wanted)) continue;
|
||||
const bool cached = std::any_of(p->rect_cache.begin(),p->rect_cache.end(),[&](const RectHash& c) {
|
||||
return c.vram == q->vram && c.rect.x == rect.x && c.rect.y == rect.y && c.rect.w == rect.w && c.rect.h == rect.h;
|
||||
});
|
||||
if (!cached) hashed_bytes += size_t(rect.w) * rect.h * 2;
|
||||
if (++groups_checked > 4096 || hashed_bytes > 4u*1024u*1024u) { over_budget = true; break; }
|
||||
const auto found = group.hashes.find(cached_hash(*p,*q,rect));
|
||||
if (found != group.hashes.end()) for (const size_t index : found->second) consider(index,r.x,r.y);
|
||||
}
|
||||
if (over_budget && !p->geometry_limit_reported) {
|
||||
note(*p,"page candidate geometry/hash budget exceeded");
|
||||
std::fprintf(stderr,"[texture-pack] page candidate geometry/hash budget exceeded; using native texture\n");
|
||||
p->geometry_limit_reported = true;
|
||||
}
|
||||
int status = over_budget ? HD_TEXTURE_LOOKUP_ERROR : winner == size_t(-1) ? HD_TEXTURE_LOOKUP_NONE :
|
||||
ambiguous ? HD_TEXTURE_LOOKUP_AMBIGUOUS : HD_TEXTURE_LOOKUP_FOUND;
|
||||
if (status == HD_TEXTURE_LOOKUP_FOUND) {
|
||||
const Entry& e = p->entries[winner]; out->key = e.key; out->entry_id = winner + 1;
|
||||
out->replacement_path = e.path.c_str(); out->origin_u = origin_u; out->origin_v = origin_v;
|
||||
out->source_width = e.key.width; out->source_height = e.key.height;
|
||||
}
|
||||
if (p->match_cache.size() >= 256) p->match_cache.erase(p->match_cache.begin());
|
||||
p->match_cache.push_back({*q,st != 0,status,*out});
|
||||
return status;
|
||||
} catch (...) { return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
void duck_texture_pack_set_decode_budget(DuckTexturePack* p, size_t bytes) {
|
||||
if (!p) return;
|
||||
std::lock_guard<std::mutex> lock(p->worker.mutex);
|
||||
p->worker.budget = bytes; p->worker.evict(0);
|
||||
}
|
||||
int duck_texture_pack_request_decode(DuckTexturePack* p, uint64_t id) {
|
||||
if (!p || !id || id > p->entries.size() || p->entries[id-1].ambiguous) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
try {
|
||||
Worker& w = p->worker; std::lock_guard<std::mutex> lock(w.mutex);
|
||||
auto it = w.decoded.find(id);
|
||||
if (it != w.decoded.end()) {
|
||||
it->second.use = ++w.tick;
|
||||
return it->second.status == DecodeStatus::Ready ? HD_TEXTURE_LOOKUP_FOUND :
|
||||
it->second.status == DecodeStatus::Failed ? HD_TEXTURE_LOOKUP_ERROR : HD_TEXTURE_LOOKUP_NONE;
|
||||
}
|
||||
if (!w.budget) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
if (w.decode_queue.size() >= kMaxDecodeQueue) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (w.decoded.size() >= 512) {
|
||||
auto victim = w.decoded.end();
|
||||
for (auto item = w.decoded.begin(); item != w.decoded.end(); ++item)
|
||||
if ((item->second.status == DecodeStatus::Ready || item->second.status == DecodeStatus::Failed) &&
|
||||
(victim == w.decoded.end() || item->second.use < victim->second.use)) victim = item;
|
||||
if (victim == w.decoded.end()) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (victim->second.image) w.used -= victim->second.image->rgba.size();
|
||||
w.decoded.erase(victim);
|
||||
}
|
||||
if (!w.thread.joinable()) w.thread = std::thread(worker_main,&w);
|
||||
w.decoded[id].path = p->entries[id-1].path; w.decode_queue.push_back(id);
|
||||
w.wake.notify_one(); return HD_TEXTURE_LOOKUP_NONE;
|
||||
} catch (...) { return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* p, uint64_t id, DuckTexturePixels* out) {
|
||||
if (out) *out = {};
|
||||
if (!p || !out) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
try {
|
||||
Worker& w = p->worker; std::lock_guard<std::mutex> lock(w.mutex); const auto it = w.decoded.find(id);
|
||||
if (it == w.decoded.end()) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (it->second.status != DecodeStatus::Ready) return it->second.status == DecodeStatus::Failed ? HD_TEXTURE_LOOKUP_ERROR : HD_TEXTURE_LOOKUP_NONE;
|
||||
auto* lease = new Lease{it->second.image}; it->second.use = ++w.tick;
|
||||
out->lease = lease; out->rgba = lease->image->rgba.data(); out->width = lease->image->w;
|
||||
out->height = lease->image->h; out->stride = out->width * 4; return HD_TEXTURE_LOOKUP_FOUND;
|
||||
} catch (...) { return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
void duck_texture_pixels_release(DuckTexturePixels* p) { if (p) { delete static_cast<Lease*>(p->lease); *p = {}; } }
|
||||
|
||||
int duck_texture_pack_dump_draw(DuckTexturePack* p, const HdTextureDrawQuery* q, int st, char* error, size_t capacity) {
|
||||
if (!p || !q) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
Rect rect{}; if (!query_rect(q,rect)) return HD_TEXTURE_LOOKUP_NONE;
|
||||
try {
|
||||
Worker& w = p->worker;
|
||||
{ std::lock_guard<std::mutex> lock(w.mutex);
|
||||
if (!w.dump_error.empty()) { error_text(error,capacity,w.dump_error); return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
if (w.dump_queue.size() >= kMaxDumpQueue) return HD_TEXTURE_LOOKUP_NONE; }
|
||||
const unsigned ppw = expansion(q->depth); const Upload* upload = nullptr;
|
||||
for (const auto& u : p->uploads) if (contains(u.rect,rect)) { if (upload) { upload = nullptr; break; } upload = &u; }
|
||||
DuckTextureKey k{}; k.kind = upload ? DUCK_TEXTURE_UPLOAD : DUCK_TEXTURE_PAGE;
|
||||
k.depth = q->depth; k.semitransparent = st != 0;
|
||||
k.source_hash = upload ? upload->hash : cached_hash(*p,*q,rect);
|
||||
k.source_width_words = upload ? upload->rect.w : 256 / ppw; k.source_height = upload ? upload->rect.h : 256;
|
||||
k.offset_x = (rect.x - (upload ? upload->rect.x : q->page_x)) * ppw;
|
||||
k.offset_y = rect.y - (upload ? upload->rect.y : q->page_y); k.width = rect.w * ppw; k.height = rect.h;
|
||||
k.palette_max = q->depth == 0 ? 15 : q->depth == 1 ? 255 : 0;
|
||||
bool palette_valid = false; k.palette_hash = palette_hash(*q,0,k.palette_max,palette_valid);
|
||||
if (!palette_valid) return HD_TEXTURE_LOOKUP_NONE;
|
||||
const std::string name = stem(k);
|
||||
if (name.empty()) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (p->dumped.count(name)) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (p->dumped.size() >= kMaxDumps) {
|
||||
if (!p->dump_limit_reported) {
|
||||
note(*p,"dump session limit (8192) reached; reset dump tracking to continue");
|
||||
std::fprintf(stderr,"[texture-pack] dump session limit (8192) reached; reset dump tracking to continue\n");
|
||||
p->dump_limit_reported = true;
|
||||
}
|
||||
error_text(error,capacity,"texture dump session limit (8192) reached");
|
||||
return HD_TEXTURE_LOOKUP_ERROR;
|
||||
}
|
||||
auto image = std::make_shared<Image>(); image->w = k.width; image->h = k.height;
|
||||
image->rgba.resize(size_t(image->w) * image->h * 4);
|
||||
for (unsigned y = 0; y < image->h; ++y) for (unsigned x = 0; x < image->w; ++x) {
|
||||
uint16_t color = q->vram[(rect.y+y)*1024 + rect.x + x/ppw];
|
||||
if (q->depth < 2) {
|
||||
const unsigned index = q->depth == 0 ? ((color >> ((x%4)*4)) & 15) : ((color >> ((x%2)*8)) & 255);
|
||||
color = q->clut_x + index < 1024 ? q->vram[q->clut_y*1024+q->clut_x+index] : 0;
|
||||
}
|
||||
uint8_t* pixel = image->rgba.data() + (size_t(y)*image->w+x)*4;
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const unsigned value = (color >> (c*5)) & 31;
|
||||
pixel[c] = static_cast<uint8_t>((value * 255u + 15u) / 31u);
|
||||
}
|
||||
pixel[3] = color == 0 ? 0 : (st && (color & 0x8000)) ? 128 : 255;
|
||||
}
|
||||
{ std::lock_guard<std::mutex> lock(w.mutex);
|
||||
if (w.dump_queue.size() >= kMaxDumpQueue) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (!w.thread.joinable()) w.thread = std::thread(worker_main,&w);
|
||||
w.dump_queue.push_back({p->dumps / (name + ".png"), std::move(image)});
|
||||
p->dumped.insert(name);
|
||||
w.wake.notify_one(); }
|
||||
return HD_TEXTURE_LOOKUP_FOUND;
|
||||
} catch (const std::exception& e) { error_text(error,capacity,e.what()); return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
catch (...) { error_text(error,capacity,"texture dump allocation failed"); return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
void duck_texture_pack_reset_dump(DuckTexturePack* p) {
|
||||
if (!p) return;
|
||||
p->dumped.clear();
|
||||
p->dump_limit_reported = false;
|
||||
std::lock_guard<std::mutex> lock(p->worker.mutex);
|
||||
p->worker.dump_error.clear();
|
||||
}
|
||||
} // extern C
|
||||
@@ -5712,6 +5712,7 @@ static void gp0_exec_cpu_to_vram(void) {
|
||||
/* 0 means max dimension */
|
||||
vram_write_w = (w == 0) ? 0x400 : (uint16_t)w;
|
||||
vram_write_h = (h == 0) ? 0x200 : (uint16_t)h;
|
||||
gr_vram_upload_begin(vram_write_x, vram_write_y, vram_write_w, vram_write_h);
|
||||
|
||||
/* A full history retains old uploads; later transfers must not append
|
||||
* to the last slot and overflow its diagnostic word counter. */
|
||||
|
||||
+266
-44
@@ -78,6 +78,7 @@
|
||||
#include "gl_scale_limits.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "mod_texture_banks.h"
|
||||
#include "frame_interpolation.h"
|
||||
#include "render_pass_plan.h"
|
||||
@@ -371,6 +372,8 @@ static int s_hr_scale = 1;
|
||||
#endif
|
||||
/* Netplay: SW@1× + GPU@s_out_scale dual write; CPU VRAM always authoritative. */
|
||||
static int s_cpu_auth_dual = 0;
|
||||
/* Independent of netplay: the replacement surface must never become VRAM. */
|
||||
static int s_hd_native_authority = 0;
|
||||
static int s_req_scale = 1; /* requested before context init */
|
||||
/* Driver limits queried at init: min(GL_MAX_TEXTURE_SIZE,
|
||||
* GL_MAX_RENDERBUFFER_SIZE, GL_MAX_VIEWPORT_DIMS); 0 before init. */
|
||||
@@ -567,7 +570,7 @@ static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0;
|
||||
* fragment shader read the noperspective varying — i.e. bit-identical to the
|
||||
* pre-feature pipeline. twin is the prim's GP0(E2h) texture window, its low 20
|
||||
* bits as a whole float (mask x, mask y, offset x, offset y; 5 bits each). */
|
||||
#define TEXV 21
|
||||
#define TEXV 26
|
||||
static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0;
|
||||
static GLuint s_blit_hi_prog = 0; /* windowed hi surface blit */
|
||||
static GLint s_uBhSrc = -1, s_uBhPass = -1, s_uBhMaskset = -1, s_uBhSrcDiv = -1;
|
||||
@@ -591,6 +594,7 @@ static float s_pq[3];
|
||||
|
||||
/* TEX program uniforms. */
|
||||
static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1;
|
||||
static GLint s_uHdTexture = -1;
|
||||
static GLint s_uPalette = -1;
|
||||
static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1;
|
||||
static GLint s_uMaskset = -1, s_uFilter = -1;
|
||||
@@ -1326,6 +1330,8 @@ static const char *TEX_VS =
|
||||
"layout(location=8) in float a_semi;\n"
|
||||
"layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n"
|
||||
"layout(location=10) in float a_twin; /* GP0(E2h) bits 0..19 */\n"
|
||||
"layout(location=11) in vec4 a_hd_source; /* page origin + native extent */\n"
|
||||
"layout(location=12) in float a_hd_mode;\n"
|
||||
"uniform float u_shift;\n"
|
||||
"uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n"
|
||||
"uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n"
|
||||
@@ -1337,12 +1343,14 @@ static const char *TEX_VS =
|
||||
"flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n"
|
||||
"flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n"
|
||||
"flat out int v_twin;\n"
|
||||
"flat out vec4 v_hd_source; flat out int v_hd_mode;\n"
|
||||
"void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\n"
|
||||
" v_persp = (a_q > 0.0) ? 1 : 0;\n"
|
||||
" v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n"
|
||||
" v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n"
|
||||
" v_semi = int(a_semi + 0.5);\n"
|
||||
" v_twin = int(a_twin + 0.5);\n"
|
||||
" v_hd_source=a_hd_source; v_hd_mode=int(a_hd_mode+0.5);\n"
|
||||
" v_limits = ivec4(floor(a_limits + 0.5));\n"
|
||||
" /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n"
|
||||
" * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n"
|
||||
@@ -1371,6 +1379,8 @@ static const char *TEX_FS =
|
||||
"flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n"
|
||||
"flat in int v_semi; /* GP0 command has semi-transparency enabled */\n"
|
||||
"flat in int v_twin; /* texture window, GP0(E2h) bits 0..19 */\n"
|
||||
"flat in vec4 v_hd_source; flat in int v_hd_mode;\n"
|
||||
"uniform sampler2D u_hd_texture;\n"
|
||||
"uniform usampler2D u_vram;\n"
|
||||
"uniform usampler2D u_palette;\n"
|
||||
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
|
||||
@@ -1427,6 +1437,17 @@ static const char *TEX_FS =
|
||||
" }\n"
|
||||
" return sum;\n"
|
||||
"}\n"
|
||||
"vec4 hd_texel(vec2 uv){\n"
|
||||
" ivec2 p=ivec2(floor(uv)); vec2 f=fract(uv);\n"
|
||||
" p &= ivec2(255);\n"
|
||||
" ivec4 tw=ivec4(v_twin&31,(v_twin>>5)&31,(v_twin>>10)&31,(v_twin>>15)&31);\n"
|
||||
" if((tw.x|tw.y)!=0) p=(p & ~(tw.xy*8)) | ((tw.zw & tw.xy)*8);\n"
|
||||
" else p=clamp(p,v_limits.xy,v_limits.zw);\n"
|
||||
" vec2 t=(vec2(p)+f-v_hd_source.xy)/v_hd_source.zw;\n"
|
||||
" ivec2 size=textureSize(u_hd_texture,0);\n"
|
||||
" ivec2 at=clamp(ivec2(floor(t*vec2(size))),ivec2(0),size-ivec2(1));\n"
|
||||
" return texelFetch(u_hd_texture,at,0);\n"
|
||||
"}\n"
|
||||
"void main(){\n"
|
||||
" int stp; vec3 rgb;\n"
|
||||
" /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n"
|
||||
@@ -1434,7 +1455,20 @@ static const char *TEX_FS =
|
||||
" * default is the PS1's affine (noperspective) mapping. */\n"
|
||||
" vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n"
|
||||
" vec2 dx=dFdx(uv), dy=dFdy(uv);\n"
|
||||
" if (u_filter == 0) {\n"
|
||||
" if (v_hd_mode != 0) {\n"
|
||||
" vec4 hd=hd_texel(uv);\n"
|
||||
" rgb=hd.rgb;\n"
|
||||
" if(v_hd_mode==1){\n"
|
||||
" if(hd.a==0.0) discard;\n"
|
||||
" int native=fetch_texel(int(floor(uv.x)),int(floor(uv.y)));\n"
|
||||
" if(native==0) discard; stp=(native>>15)&1;\n"
|
||||
" } else if(v_hd_mode==2){\n"
|
||||
" if(hd.a<128.0/255.0) discard; stp=0;\n"
|
||||
" } else {\n"
|
||||
" stp=hd.a<=242.0/255.0 ? 1 : 0;\n"
|
||||
" if(all(equal(hd,vec4(0.0))) || (stp==0 && all(equal(hd.rgb,vec3(0.0))))) discard;\n"
|
||||
" }\n"
|
||||
" } else if (u_filter == 0) {\n"
|
||||
" int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n"
|
||||
" if (raw == 0) discard;\n"
|
||||
" rgb = col5(raw);\n"
|
||||
@@ -2266,6 +2300,18 @@ static void pack_flush(void) {
|
||||
rect_clear(&s_pack_dirty);
|
||||
coh_record(GL_COH_PACK, x, y, x + w - 1, y + h - 1);
|
||||
|
||||
if (s_hd_native_authority) {
|
||||
/* All pending draws consumed the previous raw version before this
|
||||
* refresh. Never encode the HD presentation image into native words. */
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_raw_tex);
|
||||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, VRAM_W);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, PSXGL_RED_INTEGER,
|
||||
GL_UNSIGNED_SHORT, s_vram + (size_t)y * VRAM_W + x);
|
||||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_raw_fbo);
|
||||
glViewport(0, 0, VRAM_W, VRAM_H);
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
@@ -2312,7 +2358,7 @@ static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth,
|
||||
static int s_depth24_skip_up = 0;
|
||||
static DirtyRect s_d24_skip_fb; /* union of skipped MDEC FB rects (VRAM halfwords) */
|
||||
static int cpu_vram_authoritative(void) {
|
||||
return s_cpu_auth_dual || s_depth24_skip_up;
|
||||
return s_cpu_auth_dual || s_hd_native_authority || s_depth24_skip_up;
|
||||
}
|
||||
|
||||
static void ensure_cpu(void) {
|
||||
@@ -2575,6 +2621,89 @@ static int s_tb_n = 0; /* verts queued */
|
||||
static int s_tb_semi = -2;
|
||||
static int s_tb_mask = 0, s_tb_filter = 0;
|
||||
static GLuint s_tb_bank_tex;
|
||||
static GLuint s_tb_hd_tex;
|
||||
|
||||
#define HD_GL_CACHE_CAP 64
|
||||
#define HD_GL_CACHE_BUDGET (128u * 1024u * 1024u)
|
||||
typedef struct HdGlTexture {
|
||||
uint64_t generation, key;
|
||||
uint64_t last_use;
|
||||
GLuint texture;
|
||||
size_t bytes;
|
||||
} HdGlTexture;
|
||||
static HdGlTexture s_hd_gl_cache[HD_GL_CACHE_CAP];
|
||||
static size_t s_hd_gl_cache_bytes;
|
||||
static uint64_t s_hd_gl_cache_clock;
|
||||
|
||||
void gl_renderer_clear_hd_texture_cache(void) {
|
||||
flush_flat_batch(); flush_tex_batch(); hiw_flush_queue();
|
||||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i) {
|
||||
if (s_ctx && s_hd_gl_cache[i].texture)
|
||||
glDeleteTextures(1, &s_hd_gl_cache[i].texture);
|
||||
}
|
||||
memset(s_hd_gl_cache, 0, sizeof(s_hd_gl_cache));
|
||||
s_hd_gl_cache_bytes = 0; s_tb_hd_tex = 0;
|
||||
}
|
||||
static GLuint hd_gl_texture(const GpuHdTextureImage* image) {
|
||||
if (!image->rgba || !image->width || !image->height || !image->source_width ||
|
||||
!image->source_height || image->stride != (uint64_t)image->width * 4u ||
|
||||
image->width > (uint32_t)s_gl_max_dim || image->height > (uint32_t)s_gl_max_dim)
|
||||
return 0;
|
||||
size_t bytes = (size_t)image->stride * image->height;
|
||||
if (bytes > HD_GL_CACHE_BUDGET) return 0;
|
||||
int free_slot = -1;
|
||||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i) {
|
||||
HdGlTexture* texture = &s_hd_gl_cache[i];
|
||||
if (texture->texture && texture->key == image->cache_key &&
|
||||
texture->generation == image->generation) {
|
||||
texture->last_use = ++s_hd_gl_cache_clock; return texture->texture;
|
||||
}
|
||||
if (!texture->texture && free_slot < 0) free_slot = i;
|
||||
}
|
||||
while (free_slot < 0 || s_hd_gl_cache_bytes + bytes > HD_GL_CACHE_BUDGET) {
|
||||
int oldest = -1;
|
||||
for (int i = 0; i < HD_GL_CACHE_CAP; ++i)
|
||||
if (s_hd_gl_cache[i].texture && (oldest < 0 ||
|
||||
s_hd_gl_cache[i].last_use < s_hd_gl_cache[oldest].last_use)) oldest = i;
|
||||
if (oldest < 0) return 0;
|
||||
flush_flat_batch(); flush_tex_batch(); hiw_flush_queue();
|
||||
HdGlTexture* victim = &s_hd_gl_cache[oldest];
|
||||
glDeleteTextures(1, &victim->texture);
|
||||
s_hd_gl_cache_bytes -= victim->bytes;
|
||||
memset(victim, 0, sizeof(*victim)); free_slot = oldest;
|
||||
}
|
||||
/* A newly allocated image cannot affect pending draws, but TextureImage
|
||||
* changes GL bindings; their flush always binds its own complete state. */
|
||||
HdGlTexture* texture = &s_hd_gl_cache[free_slot];
|
||||
glGenTextures(1, &texture->texture);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 2);
|
||||
glBindTexture(GL_TEXTURE_2D, texture->texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(PSXGL_UNPACK_ROW_LENGTH, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, image->width, image->height, 0,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, image->rgba);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
if (glGetError() != GL_NO_ERROR) {
|
||||
glDeleteTextures(1, &texture->texture); texture->texture = 0; return 0;
|
||||
}
|
||||
texture->key = image->cache_key; texture->generation = image->generation;
|
||||
texture->last_use = ++s_hd_gl_cache_clock;
|
||||
texture->bytes = bytes; s_hd_gl_cache_bytes += bytes;
|
||||
return texture->texture;
|
||||
}
|
||||
|
||||
static void bind_textured_resources(GLuint source, GLuint palette, GLuint hd) {
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, source); p_glUniform1i(s_uVram, 0);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 1);
|
||||
glBindTexture(GL_TEXTURE_2D, palette); p_glUniform1i(s_uPalette, 1);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 2);
|
||||
glBindTexture(GL_TEXTURE_2D, hd ? hd : s_up_tex); p_glUniform1i(s_uHdTexture, 2);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
}
|
||||
static int s_tb_twin[4] = {0, 0, 0, 0};
|
||||
static uint64_t s_batch_total = 0, s_batch_reason[7];
|
||||
|
||||
@@ -2729,6 +2858,7 @@ typedef struct {
|
||||
uint8_t check; /* mask-check state at draw time */
|
||||
uint8_t filter;
|
||||
GLuint tex; /* sampled texture (bank or raw mirror) */
|
||||
GLuint palette, hd_tex;
|
||||
int ax0, ay0, ax1, ay1; /* draw area, inclusive */
|
||||
size_t vfirst; /* float offset into s_hq_v */
|
||||
int vcount; /* vertices */
|
||||
@@ -2801,6 +2931,8 @@ static int hiw_enqueue_tex(int nverts, int semi, int mirror, int gate) {
|
||||
c->mask = (uint8_t)s_tb_mask;
|
||||
c->filter = (uint8_t)s_tb_filter;
|
||||
c->tex = s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex;
|
||||
c->palette = s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex;
|
||||
c->hd_tex = s_tb_hd_tex;
|
||||
if (wq) hiw_wide_set(c, gate);
|
||||
return wq;
|
||||
}
|
||||
@@ -2857,7 +2989,7 @@ static void hiw_replay_wide(void) {
|
||||
p_glUniform1f(s_tex_uXoff, (float)c->wdx);
|
||||
p_glUniform1f(s_tex_uXscale, c->wscale);
|
||||
p_glUniform1f(s_tex_uXcenter, c->wcenter);
|
||||
glBindTexture(GL_TEXTURE_2D, c->tex);
|
||||
bind_textured_resources(c->tex, c->palette, c->hd_tex);
|
||||
p_glUniform1i(s_uMaskset, c->mask);
|
||||
p_glUniform1i(s_uFilter, c->filter);
|
||||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)),
|
||||
@@ -2933,7 +3065,7 @@ static void hiw_flush_queue(void) {
|
||||
p_glBindBuffer(PSXGL_ARRAY_BUFFER, s_tex_vbo);
|
||||
cur = HQ_TEX;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, c->tex);
|
||||
bind_textured_resources(c->tex, c->palette, c->hd_tex);
|
||||
p_glUniform1i(s_uMaskset, c->mask);
|
||||
p_glUniform1i(s_uFilter, c->filter);
|
||||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)),
|
||||
@@ -2978,13 +3110,8 @@ static void flush_tex_batch(void) {
|
||||
|
||||
hr_begin(1);
|
||||
p_glUseProgram(s_tex_prog);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex);
|
||||
p_glUniform1i(s_uVram, 0);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 1);
|
||||
glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex);
|
||||
p_glUniform1i(s_uPalette, 1);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
bind_textured_resources(s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex,
|
||||
s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex, s_tb_hd_tex);
|
||||
p_glUniform1i(s_uMaskset, s_tb_mask);
|
||||
p_glUniform1i(s_uFilter, s_tb_filter);
|
||||
p_glBindVertexArray(s_tex_vao);
|
||||
@@ -3386,6 +3513,18 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y);
|
||||
else if (s_selected_bank_live_clut)
|
||||
flush_pack_if_sampling(-1, 0, depth, clut_x, clut_y);
|
||||
GpuHdTextureImage hd = {0};
|
||||
GLuint hd_tex = 0;
|
||||
if (s_hd_native_authority && !s_selected_bank_tex) {
|
||||
uint32_t window = (uint32_t)(s_tw_mask_x | (s_tw_mask_y << 5) |
|
||||
(s_tw_off_x << 10) | (s_tw_off_y << 15));
|
||||
if (gpu_hd_textures_acquire_draw(texpage, clut_x, clut_y, lim, window,
|
||||
semi >= 0, &hd)) {
|
||||
hd_tex = hd_gl_texture(&hd);
|
||||
if (!hd_tex) hd.alpha_mode = 0;
|
||||
else gpu_hd_textures_note_applied();
|
||||
}
|
||||
}
|
||||
mark_prim_dirty(xs, ys, 3, 1 /* textured */);
|
||||
|
||||
/* Append to the textured batch. Flush first if this prim's blend/mask/twin/
|
||||
@@ -3426,7 +3565,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
!mod_texture_bank_batchable(s_selected_bank_tex != 0, s_mask_check, semi);
|
||||
int reason = -1;
|
||||
if (s_tb_n > 0) {
|
||||
if (s_tb_bank_tex != s_selected_bank_tex || s_tb_bank_live_clut != s_selected_bank_live_clut) reason = 0;
|
||||
if (s_tb_bank_tex != s_selected_bank_tex || s_tb_bank_live_clut != s_selected_bank_live_clut || s_tb_hd_tex != hd_tex) reason = 0;
|
||||
else if (isolate) reason = 0;
|
||||
else if (batch_semi != s_tb_semi) reason = 1;
|
||||
else if (s_mask_set != s_tb_mask) reason = 2;
|
||||
@@ -3445,6 +3584,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = filter; s_tb_gate = gate;
|
||||
s_tb_bank_tex = s_selected_bank_tex;
|
||||
s_tb_bank_live_clut = s_selected_bank_live_clut;
|
||||
s_tb_hd_tex = hd_tex;
|
||||
s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy;
|
||||
}
|
||||
/* The prim's window, exact as a float (20 bits). */
|
||||
@@ -3464,10 +3604,14 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */
|
||||
vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */
|
||||
vp[20] = twin; /* a_twin */
|
||||
vp[21] = (float)hd.origin_u; vp[22] = (float)hd.origin_v;
|
||||
vp[23] = (float)hd.source_width; vp[24] = (float)hd.source_height;
|
||||
vp[25] = (float)hd.alpha_mode;
|
||||
}
|
||||
s_tb_n += 3;
|
||||
if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */
|
||||
}
|
||||
gpu_hd_textures_release_image(&hd);
|
||||
}
|
||||
|
||||
/* Draw a flat-colored rect (GEO program) DIRECTLY into the active wide surface
|
||||
@@ -3780,8 +3924,17 @@ static int cpu_raster_required(void) {
|
||||
* GPU surface. The CPU mirror stays the authoritative record of the real
|
||||
* frame, so the pass never rasterizes into it. depth24 scanout still
|
||||
* presents from the CPU mirror and keeps its write-through. */
|
||||
return !s_raster_ok || s_depth24_skip_up || (s_cpu_auth_dual && !s_pass_active);
|
||||
return !s_raster_ok || s_depth24_skip_up ||
|
||||
((s_cpu_auth_dual || s_hd_native_authority) && !s_pass_active);
|
||||
}
|
||||
/* Bracket each authoritative draw so a later netplay/session policy change
|
||||
* cannot accidentally disable the faithful floor for HD replacement mode. */
|
||||
static int native_draw_begin(void) {
|
||||
int previous = sw_faithful_authority();
|
||||
if (s_hd_native_authority) sw_set_faithful_authority(1);
|
||||
return previous;
|
||||
}
|
||||
static void native_draw_end(int previous) { sw_set_faithful_authority(previous); }
|
||||
/* The sub-pixel / perspective override describes exactly one triangle; drop it
|
||||
* once that triangle has been submitted so a later prim can never inherit it. */
|
||||
static inline void precise_consumed(void) { s_pc_valid = 0; s_pq_valid = 0; s_projected_uv_valid = 0; }
|
||||
@@ -3824,80 +3977,127 @@ void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3],
|
||||
}
|
||||
|
||||
static void glb_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col) {
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_flat_triangle(x0,y0,x1,y1,x2,y2,col);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_triangle(x0,y0,col, x1,y1,col, x2,y2,col, s_semi_en?s_semi_mode:-1);
|
||||
precise_consumed();
|
||||
}
|
||||
static void glb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2) {
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_gouraud_triangle(x0,y0,c0,x1,y1,c1,x2,y2,c2);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_triangle(x0,y0,c0, x1,y1,c1, x2,y2,c2, s_semi_en?s_semi_mode:-1);
|
||||
precise_consumed();
|
||||
}
|
||||
static void glb_fill_rect(int x,int y,int w,int h,uint16_t c){
|
||||
if (pass_refuse_write("fill", x, y, w, h)) return;
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_fill_rect(x,y,w,h,c);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_fill(x,y,w,h,c);
|
||||
}
|
||||
static void glb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){
|
||||
if (pass_refuse_write("copy", dx, dy, w, h)) return;
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_copy_rect(sx,sy,dx,dy,w,h);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_copy_rect(sx,sy,dx,dy,w,h);
|
||||
}
|
||||
static void glb_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp){
|
||||
if (cpu_raster_required())
|
||||
const int native_required = cpu_raster_required();
|
||||
if (native_required && !s_hd_native_authority)
|
||||
sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp);
|
||||
if (!s_raster_ok) return;
|
||||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||||
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
|
||||
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
|
||||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL);
|
||||
if (s_raster_ok) {
|
||||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||||
float mr=s_mod_r/255.0f, mg=s_mod_g/255.0f, mb=s_mod_b/255.0f;
|
||||
float col[9]={mr,mg,mb, mr,mg,mb, mr,mg,mb};
|
||||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,s_mod_raw, s_semi_en?s_semi_mode:-1, NULL);
|
||||
}
|
||||
if (native_required && s_hd_native_authority) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_textured_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,cx,cy,tp);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
precise_consumed();
|
||||
}
|
||||
static void glb_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t cx,uint16_t cy,uint16_t tp,int raw){
|
||||
if (cpu_raster_required())
|
||||
const int native_required = cpu_raster_required();
|
||||
if (native_required && !s_hd_native_authority)
|
||||
sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw);
|
||||
if (!s_raster_ok) return;
|
||||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||||
uint32_t cc[3]={c0,c1,c2}; float col[9];
|
||||
for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; }
|
||||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL);
|
||||
if (s_raster_ok) {
|
||||
int xs[3]={x0,x1,x2}, ys[3]={y0,y1,y2}, us[3]={u0,u1,u2}, vs[3]={v0,v1,v2};
|
||||
uint32_t cc[3]={c0,c1,c2}; float col[9];
|
||||
for (int i=0;i<3;i++){ col[i*3+0]=(cc[i]&0xFF)/255.0f; col[i*3+1]=((cc[i]>>8)&0xFF)/255.0f; col[i*3+2]=((cc[i]>>16)&0xFF)/255.0f; }
|
||||
gpu_textured_triangle(xs,ys,us,vs,col,tp,cx,cy,raw, s_semi_en?s_semi_mode:-1, NULL);
|
||||
}
|
||||
if (native_required && s_hd_native_authority) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_shaded_textured_triangle(x0,y0,u0,v0,c0,x1,y1,u1,v1,c1,x2,y2,u2,v2,c2,cx,cy,tp,raw);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
precise_consumed();
|
||||
}
|
||||
static void glb_draw_flat_rect(int x,int y,int w,int h,uint16_t c){
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_flat_rect(x,y,w,h,c);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_flat_rect(x,y,w,h,c, s_semi_en?s_semi_mode:-1);
|
||||
}
|
||||
static void glb_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t cx,uint16_t cy,uint16_t tp){
|
||||
if (cpu_raster_required())
|
||||
const int native_required = cpu_raster_required();
|
||||
if (native_required && !s_hd_native_authority)
|
||||
sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp);
|
||||
if (!s_raster_ok) return;
|
||||
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||||
if (s_raster_ok)
|
||||
gpu_textured_rect(x,y,w,h, u,v, u+w,v+h, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||||
if (native_required && s_hd_native_authority) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_textured_rect(x,y,w,h,u,v,cx,cy,tp);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
}
|
||||
static void glb_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp){
|
||||
if (cpu_raster_required())
|
||||
const int native_required = cpu_raster_required();
|
||||
if (native_required && !s_hd_native_authority)
|
||||
sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp);
|
||||
if (!s_raster_ok) return;
|
||||
gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||||
if (s_raster_ok)
|
||||
gpu_textured_rect(x,y,w,h, u0,v0, u1,v1, cx,cy,tp, s_semi_en?s_semi_mode:-1);
|
||||
if (native_required && s_hd_native_authority) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_textured_rect_scaled(x,y,w,h,u0,v0,u1,v1,cx,cy,tp);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
}
|
||||
static void glb_draw_line(int x0,int y0,int x1,int y1,uint16_t c){
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_line(x0,y0,x1,y1,c);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_line(x0,y0,c, x1,y1,c, s_semi_en?s_semi_mode:-1);
|
||||
}
|
||||
static void glb_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1){
|
||||
if (cpu_raster_required())
|
||||
if (cpu_raster_required()) {
|
||||
int faithful = native_draw_begin();
|
||||
sw_draw_shaded_line(x0,y0,c0,x1,y1,c1);
|
||||
native_draw_end(faithful);
|
||||
}
|
||||
if (!s_raster_ok) return;
|
||||
gpu_line(x0,y0,c0, x1,y1,c1, s_semi_en?s_semi_mode:-1);
|
||||
}
|
||||
@@ -4010,7 +4210,7 @@ static void depth24_clear_skipped_fb(void) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
rect_add(&s_pack_dirty, x0, y0, x1, y1);
|
||||
if (!s_cpu_auth_dual) {
|
||||
if (!cpu_vram_authoritative()) {
|
||||
rect_add(&s_cpu_dirty, x0, y0, x1, y1);
|
||||
s_gpu_dirty = 1; /* The clear must reach an immediate CPU read too. */
|
||||
}
|
||||
@@ -4446,6 +4646,7 @@ static int init_gpu_raster(void) {
|
||||
s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset");
|
||||
s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter");
|
||||
s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits");
|
||||
s_uHdTexture = p_glGetUniformLocation(s_tex_prog, "u_hd_texture");
|
||||
s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src");
|
||||
s_uBlitPass = p_glGetUniformLocation(s_blit_prog, "u_stp_pass");
|
||||
s_uBlitMaskset = p_glGetUniformLocation(s_blit_prog, "u_maskset");
|
||||
@@ -4536,6 +4737,8 @@ static int init_gpu_raster(void) {
|
||||
p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */
|
||||
p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */
|
||||
p_glVertexAttribPointer(10, 1, GL_FLOAT, GL_FALSE, st, (void*)(20*sizeof(float))); p_glEnableVertexAttribArray(10); /* twin */
|
||||
p_glVertexAttribPointer(11, 4, GL_FLOAT, GL_FALSE, st, (void*)(21*sizeof(float))); p_glEnableVertexAttribArray(11); /* HD source */
|
||||
p_glVertexAttribPointer(12, 1, GL_FLOAT, GL_FALSE, st, (void*)(25*sizeof(float))); p_glEnableVertexAttribArray(12); /* HD alpha mode */
|
||||
}
|
||||
|
||||
p_glGenVertexArrays(1, &s_blit_vao);
|
||||
@@ -4589,7 +4792,7 @@ static int init_gpu_raster(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual; }
|
||||
int gl_renderer_texture_banks_supported(void) { return s_raster_ok && !s_cpu_auth_dual && !s_hd_native_authority; }
|
||||
|
||||
int gl_renderer_fit_wide_aspect(int disp_w, int *num, int *den) {
|
||||
if (!s_raster_ok || s_gl_max_dim <= 0) return 0;
|
||||
@@ -4851,6 +5054,7 @@ static void pass_resources_release(void);
|
||||
|
||||
void gl_renderer_shutdown(void) {
|
||||
s_native_surface_enabled=s_native_surface_pending=0;
|
||||
gl_renderer_clear_hd_texture_cache();
|
||||
pass_resources_release();
|
||||
if (s_ctx) {
|
||||
for (unsigned i = 1; i < 65536u; ++i)
|
||||
@@ -5018,6 +5222,7 @@ void gl_renderer_invalidate_present(void) {
|
||||
}
|
||||
|
||||
void gl_renderer_restage_vram_after_savestate(void) {
|
||||
gpu_hd_textures_reset_tracking();
|
||||
if (!s_raster_ok || !s_vram) return;
|
||||
/* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO
|
||||
* even if a depth24 skip swallowed the restore, then re-arm scanout-band
|
||||
@@ -5032,7 +5237,7 @@ void gl_renderer_restage_vram_after_savestate(void) {
|
||||
depth24_mark_scanout_band();
|
||||
}
|
||||
/* Dual-raster: CPU is authority after snap; FBO is cosmetics only. */
|
||||
if (s_cpu_auth_dual) {
|
||||
if (s_cpu_auth_dual || s_hd_native_authority) {
|
||||
s_gpu_dirty = 0;
|
||||
rect_clear(&s_cpu_dirty);
|
||||
}
|
||||
@@ -5046,6 +5251,23 @@ void gl_renderer_set_cpu_auth_dual(int on) {
|
||||
}
|
||||
}
|
||||
|
||||
void gl_renderer_set_hd_texture_mode(int on) {
|
||||
on = on ? 1 : 0;
|
||||
if (on == s_hd_native_authority) return;
|
||||
if (s_raster_ok) {
|
||||
flush_flat_batch(); flush_tex_batch(); flush_cpu_upload(); hiw_flush_queue();
|
||||
if (on) ensure_cpu();
|
||||
else {
|
||||
/* Return GPU authority only after replacing the presentation
|
||||
* image with native VRAM; an HD frame must never be read back. */
|
||||
gl_renderer_restage_vram_after_savestate();
|
||||
}
|
||||
}
|
||||
s_hd_native_authority = on;
|
||||
s_gpu_dirty = 0; rect_clear(&s_cpu_dirty);
|
||||
if (on) s_selected_bank_tex = 0;
|
||||
}
|
||||
|
||||
int gl_renderer_cpu_auth_dual(void) {
|
||||
return s_cpu_auth_dual;
|
||||
}
|
||||
@@ -6172,7 +6394,7 @@ uint32_t gl_renderer_pass_unavailable(void) {
|
||||
s_interp_source != 1 || !(s_interp_source_hz > 0.0))
|
||||
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
|
||||
/* Dual raster (netplay CPU-authoritative VRAM) or a debug refusal. */
|
||||
if ((s_cpu_auth_dual && !render_pass_netplay_enabled()) || s_pass_force_refuse)
|
||||
if (s_hd_native_authority || (s_cpu_auth_dual && !render_pass_netplay_enabled()) || s_pass_force_refuse)
|
||||
return PSX_MOD_RENDER_PASS_BACKEND;
|
||||
/* Windowed high-resolution mode: the presented surfaces are the window
|
||||
* tiles at s_out_scale, which a pass (backing up s_hr_fbo at
|
||||
@@ -6711,7 +6933,7 @@ uint32_t gl_renderer_stereo_unavailable(void) {
|
||||
pass_refusal_init();
|
||||
if (!s_ctx || !s_raster_ok || !s_hr_fbo || gpu_display_is_depth24())
|
||||
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
|
||||
if (s_cpu_auth_dual || s_hiw || g_wide_w > 0 || s_pass_force_refuse > 0)
|
||||
if (s_cpu_auth_dual || s_hd_native_authority || s_hiw || g_wide_w > 0 || s_pass_force_refuse > 0)
|
||||
return PSX_MOD_RENDER_PASS_BACKEND;
|
||||
return PSX_MOD_RENDER_PASS_READY;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "hd_texture_pack.h"
|
||||
#include "duckstation_texture_pack.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
constexpr size_t kVramWords = 1024u * 512u;
|
||||
struct Session {
|
||||
std::string root;
|
||||
HdTexturePack* beetle = nullptr;
|
||||
DuckTexturePack* duck = nullptr;
|
||||
bool replacements = false;
|
||||
bool dump = false;
|
||||
bool dump_failed = false;
|
||||
GpuHdTextureDiag diag{};
|
||||
~Session() { hd_texture_pack_destroy(beetle); duck_texture_pack_destroy(duck); }
|
||||
};
|
||||
struct Lease {
|
||||
HdTexturePixels beetle{};
|
||||
DuckTexturePixels duck{};
|
||||
~Lease() { hd_texture_pixels_release(&beetle); duck_texture_pixels_release(&duck); }
|
||||
};
|
||||
std::unique_ptr<Session> session;
|
||||
const uint16_t* native_vram = nullptr;
|
||||
uint64_t generation = 1;
|
||||
|
||||
void fail(char* error, size_t capacity, const char* message) {
|
||||
if (error && capacity) std::snprintf(error, capacity, "%s", message);
|
||||
}
|
||||
bool is_beetle_root(const std::filesystem::path& root) {
|
||||
std::error_code error;
|
||||
if (std::filesystem::exists(root / "Hashes.ini", error)) return true;
|
||||
if (std::filesystem::exists(root.parent_path() / "Hashes.ini", error)) return true;
|
||||
const auto name = root.filename().u8string();
|
||||
const std::string suffix = "-texture-replacements";
|
||||
return name.size() >= suffix.size() &&
|
||||
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||||
}
|
||||
int window_coord(int value, int mask, int offset) {
|
||||
return (value & ~(mask * 8)) | ((offset & mask) * 8);
|
||||
}
|
||||
bool make_query(uint16_t texpage, uint16_t cx, uint16_t cy, const int limits[4],
|
||||
uint32_t window, HdTextureDrawQuery& query) {
|
||||
if (!native_vram || !limits || ((texpage >> 7) & 3u) == 3u) return false;
|
||||
if (limits[0] < 0 || limits[1] < 0 || limits[2] > 255 || limits[3] > 255 ||
|
||||
limits[0] > limits[2] || limits[1] > limits[3]) return false;
|
||||
int first[2] = {255,255}, last[2] = {0,0};
|
||||
for (int axis = 0; axis < 2; ++axis) {
|
||||
int mask = (window >> (axis * 5)) & 31;
|
||||
int offset = (window >> (10 + axis * 5)) & 31;
|
||||
for (int p = limits[axis]; p <= limits[axis + 2]; ++p) {
|
||||
const int mapped = window_coord(p, mask, offset);
|
||||
first[axis] = std::min(first[axis], mapped);
|
||||
last[axis] = std::max(last[axis], mapped);
|
||||
}
|
||||
}
|
||||
query = {};
|
||||
query.page_x = (texpage & 15u) * 64u;
|
||||
query.page_y = ((texpage >> 4) & 1u) * 256u;
|
||||
query.depth = (texpage >> 7) & 3u;
|
||||
query.u_first = static_cast<uint8_t>(first[0]);
|
||||
query.u_last = static_cast<uint8_t>(last[0]);
|
||||
query.v_first = static_cast<uint8_t>(first[1]);
|
||||
query.v_last = static_cast<uint8_t>(last[1]);
|
||||
query.clut_x = cx; query.clut_y = cy;
|
||||
query.vram = native_vram; query.vram_word_count = kVramWords;
|
||||
return true;
|
||||
}
|
||||
void dump_query(const HdTextureDrawQuery& query, int semi) {
|
||||
if (!session->dump || session->dump_failed) return;
|
||||
char error[256]{};
|
||||
const int status = duck_texture_pack_dump_draw(session->duck, &query, semi, error, sizeof(error));
|
||||
if (status == HD_TEXTURE_LOOKUP_FOUND) ++session->diag.dumped_textures;
|
||||
if (status < 0) {
|
||||
std::fprintf(stderr, "psxrecomp: texture dump stopped: %s\n", error);
|
||||
/* Keep the configured raster authority until a safe session boundary.
|
||||
* Tearing it down inside a pre-draw query would lose command order. */
|
||||
session->dump_failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int gpu_hd_textures_configure(const char* root, int replacements,
|
||||
int dump, char* error, size_t capacity) {
|
||||
if (error && capacity) error[0] = 0;
|
||||
if ((!root || !root[0]) && !replacements && !dump) {
|
||||
gpu_hd_textures_shutdown(); return 1;
|
||||
}
|
||||
if (!root || !root[0]) { fail(error, capacity, "Select an external texture-pack directory."); return 0; }
|
||||
try {
|
||||
auto next = std::make_unique<Session>();
|
||||
next->root = root; next->replacements = replacements != 0; next->dump = dump != 0;
|
||||
const bool beetle = is_beetle_root(std::filesystem::u8path(root));
|
||||
if (beetle) {
|
||||
if (!hd_texture_pack_create(root, &next->beetle, error, capacity)) return 0;
|
||||
} else {
|
||||
char ignored[256]{};
|
||||
hd_texture_pack_create(root, &next->beetle, ignored, sizeof(ignored));
|
||||
}
|
||||
if (!next->beetle) {
|
||||
auto directory = std::filesystem::u8path(root);
|
||||
if (directory.filename().u8string() == "replacements") directory = directory.parent_path();
|
||||
std::filesystem::create_directories(directory / "replacements");
|
||||
std::filesystem::create_directories(directory / "dumps");
|
||||
}
|
||||
if (!duck_texture_pack_create(root, &next->duck, error, capacity)) return 0;
|
||||
if (session && session->root == next->root) {
|
||||
if (!duck_texture_pack_copy_tracking(next->duck, session->duck)) {
|
||||
fail(error, capacity, "Could not preserve texture-upload tracking during reload."); return 0;
|
||||
}
|
||||
if (session->beetle && next->beetle) {
|
||||
uint8_t* saved = nullptr; size_t saved_size = 0;
|
||||
if (!hd_texture_pack_tracking_state_save(session->beetle, &saved, &saved_size)) {
|
||||
fail(error, capacity, "Could not save Beetle texture-upload tracking during reload."); return 0;
|
||||
}
|
||||
const int restored = hd_texture_pack_tracking_state_load(next->beetle, saved, saved_size);
|
||||
std::free(saved);
|
||||
if (!restored) { fail(error, capacity, "Could not restore Beetle texture-upload tracking during reload."); return 0; }
|
||||
}
|
||||
}
|
||||
/* This also flushes all queued users before old replacement textures
|
||||
* and pixel leases may be destroyed. */
|
||||
gl_renderer_set_hd_texture_mode(next->replacements || next->dump);
|
||||
gl_renderer_clear_hd_texture_cache();
|
||||
session = std::move(next); ++generation;
|
||||
DuckTexturePackInfo info{};
|
||||
duck_texture_pack_get_info(session->duck, &info);
|
||||
size_t replacements_found = info.replacement_count;
|
||||
size_t ambiguous = info.ambiguous_count;
|
||||
if (session->beetle) {
|
||||
HdTexturePackInfo beetle_info{};
|
||||
hd_texture_pack_get_info(session->beetle, &beetle_info);
|
||||
replacements_found = beetle_info.unique_key_count;
|
||||
ambiguous = beetle_info.ambiguous_key_count;
|
||||
}
|
||||
std::fprintf(stdout, "psxrecomp: texture pack '%s': %zu replacements, %zu ambiguous, %zu ignored\n",
|
||||
session->root.c_str(), replacements_found, ambiguous, info.ignored_count);
|
||||
if (info.diagnostic && info.diagnostic[0])
|
||||
std::fprintf(stderr, "psxrecomp: texture pack: %s\n", info.diagnostic);
|
||||
return 1;
|
||||
} catch (const std::exception& exception) {
|
||||
fail(error, capacity, exception.what()); return 0;
|
||||
}
|
||||
}
|
||||
extern "C" void gpu_hd_textures_shutdown(void) {
|
||||
gl_renderer_set_hd_texture_mode(0);
|
||||
gl_renderer_clear_hd_texture_cache();
|
||||
session.reset(); ++generation;
|
||||
}
|
||||
extern "C" int gpu_hd_textures_reload(char* error, size_t capacity) {
|
||||
if (!session) { fail(error, capacity, "No texture-pack session is active."); return 0; }
|
||||
const std::string root = session->root;
|
||||
return gpu_hd_textures_configure(root.c_str(), session->replacements, session->dump, error, capacity);
|
||||
}
|
||||
extern "C" void gpu_hd_textures_set_dump_enabled(int enabled) {
|
||||
if (!session || session->dump == (enabled != 0)) return;
|
||||
gl_renderer_set_hd_texture_mode(session->replacements || enabled);
|
||||
session->dump = enabled != 0;
|
||||
session->dump_failed = false;
|
||||
}
|
||||
extern "C" int gpu_hd_textures_replacements_enabled(void) { return session && session->replacements; }
|
||||
extern "C" int gpu_hd_textures_dump_enabled(void) { return session && session->dump; }
|
||||
extern "C" int gpu_hd_textures_active(void) { return session && (session->replacements || session->dump); }
|
||||
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) {
|
||||
if (!out) return;
|
||||
*out = {};
|
||||
if (!session) { out->root = ""; return; }
|
||||
*out = session->diag;
|
||||
out->root = session->root.c_str(); out->active = gpu_hd_textures_active();
|
||||
out->replacements = session->replacements; out->dump = session->dump && !session->dump_failed;
|
||||
out->format = session->beetle ? 1 : 2;
|
||||
if (session->beetle) { HdTexturePackInfo info{}; hd_texture_pack_get_info(session->beetle, &info); out->replacement_count = info.unique_key_count; }
|
||||
else { DuckTexturePackInfo info{}; duck_texture_pack_get_info(session->duck, &info); out->replacement_count = info.replacement_count; }
|
||||
}
|
||||
extern "C" void gpu_hd_textures_note_applied(void) { if (session) ++session->diag.applied_draws; }
|
||||
extern "C" void gpu_hd_textures_set_vram(const uint16_t* vram) { native_vram = vram; gpu_hd_textures_reset_tracking(); }
|
||||
extern "C" void gpu_hd_textures_reset_tracking(void) {
|
||||
if (!session) return;
|
||||
hd_texture_pack_reset_tracking(session->beetle);
|
||||
duck_texture_pack_reset_tracking(session->duck);
|
||||
}
|
||||
extern "C" void gpu_hd_textures_invalidate(int x, int y, int width, int height) {
|
||||
if (!session || width < 1 || height < 1) return;
|
||||
if (width > 1024 || height > 512) { gpu_hd_textures_reset_tracking(); return; }
|
||||
hd_texture_pack_invalidate(session->beetle, x & 1023, y & 511, width, height);
|
||||
duck_texture_pack_invalidate(session->duck, x & 1023, y & 511, width, height);
|
||||
}
|
||||
extern "C" void gpu_hd_textures_track_upload(int x, int y, int width, int height,
|
||||
const uint16_t* words) {
|
||||
if (!session || !native_vram || !words || width < 1 || width > 1024 || height < 1 || height > 512) return;
|
||||
if (width == 1024 && height == 512) { gpu_hd_textures_reset_tracking(); return; }
|
||||
/* gpu.c's A0 staging already contains post-mask words. Read the canonical
|
||||
* mirror here also for host-initiated transfers which use raw input. */
|
||||
const size_t count = static_cast<size_t>(width) * height;
|
||||
std::unique_ptr<uint16_t[]> applied(new (std::nothrow) uint16_t[count]);
|
||||
if (!applied) { gpu_hd_textures_reset_tracking(); return; }
|
||||
for (int row = 0; row < height; ++row)
|
||||
for (int col = 0; col < width; ++col)
|
||||
applied[static_cast<size_t>(row) * width + col] =
|
||||
native_vram[((y + row) & 511) * 1024 + ((x + col) & 1023)];
|
||||
hd_texture_pack_track_upload(session->beetle, x & 1023, y & 511, width, height,
|
||||
applied.get(), count, nullptr);
|
||||
duck_texture_pack_track_upload(session->duck, x & 1023, y & 511, width, height,
|
||||
native_vram, kVramWords);
|
||||
}
|
||||
extern "C" int gpu_hd_textures_acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy,
|
||||
const int limits[4], uint32_t window,
|
||||
int semi, GpuHdTextureImage* image) {
|
||||
if (!image) return 0;
|
||||
*image = {};
|
||||
if (!session) return 0;
|
||||
++session->diag.draw_queries;
|
||||
HdTextureDrawQuery query{};
|
||||
if (!make_query(tp, cx, cy, limits, window, query)) return 0;
|
||||
dump_query(query, semi);
|
||||
if (!session->replacements) return 0;
|
||||
auto lease = std::unique_ptr<Lease>(new (std::nothrow) Lease);
|
||||
if (!lease) return 0;
|
||||
if (session->beetle) {
|
||||
HdTextureMatch match{};
|
||||
if (hd_texture_pack_match(session->beetle, &query, &match) != HD_TEXTURE_LOOKUP_FOUND) return 0;
|
||||
++session->diag.matched_draws;
|
||||
hd_texture_pack_request_decode(session->beetle, match.entry.texture_hash, match.entry.palette_hash);
|
||||
if (hd_texture_pack_acquire_decoded(session->beetle, match.entry.texture_hash,
|
||||
match.entry.palette_hash, &lease->beetle) != HD_TEXTURE_LOOKUP_FOUND) return 0;
|
||||
const unsigned pixels_per_word = 4u >> query.depth;
|
||||
image->rgba = lease->beetle.rgba; image->width = lease->beetle.width;
|
||||
image->height = lease->beetle.height; image->stride = lease->beetle.stride;
|
||||
image->source_width = match.upload_width_words * pixels_per_word;
|
||||
image->source_height = match.upload_height;
|
||||
image->origin_u = query.u_first - (match.source_word_x * pixels_per_word + (query.u_first % pixels_per_word));
|
||||
image->origin_v = query.v_first - match.source_y;
|
||||
image->cache_key = (static_cast<uint64_t>(match.entry.texture_hash) << 32) | match.entry.palette_hash;
|
||||
image->alpha_mode = 1;
|
||||
} else {
|
||||
DuckTextureMatch match{};
|
||||
if (duck_texture_pack_match_draw(session->duck, &query, semi, &match) != HD_TEXTURE_LOOKUP_FOUND) return 0;
|
||||
++session->diag.matched_draws;
|
||||
duck_texture_pack_request_decode(session->duck, match.entry_id);
|
||||
if (duck_texture_pack_acquire_decoded(session->duck, match.entry_id, &lease->duck) != HD_TEXTURE_LOOKUP_FOUND) return 0;
|
||||
image->rgba = lease->duck.rgba; image->width = lease->duck.width;
|
||||
image->height = lease->duck.height; image->stride = lease->duck.stride;
|
||||
image->source_width = match.source_width; image->source_height = match.source_height;
|
||||
image->origin_u = match.origin_u; image->origin_v = match.origin_v;
|
||||
image->cache_key = match.entry_id;
|
||||
image->alpha_mode = match.key.semitransparent ? 3 : 2;
|
||||
}
|
||||
image->generation = generation; image->lease = lease.release();
|
||||
++session->diag.ready_draws;
|
||||
return 1;
|
||||
}
|
||||
extern "C" void gpu_hd_textures_release_image(GpuHdTextureImage* image) {
|
||||
if (!image) return;
|
||||
delete static_cast<Lease*>(image->lease); *image = {};
|
||||
}
|
||||
extern "C" void gpu_hd_textures_observe_draw(uint16_t tp, uint16_t cx, uint16_t cy,
|
||||
const int limits[4], uint32_t window, int semi) {
|
||||
if (!session || !session->dump || session->dump_failed) return;
|
||||
HdTextureDrawQuery query{};
|
||||
if (make_query(tp, cx, cy, limits, window, query)) {
|
||||
++session->diag.draw_queries; dump_query(query, semi);
|
||||
}
|
||||
}
|
||||
+70
-10
@@ -12,6 +12,8 @@
|
||||
|
||||
#include "gpu_render.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "gpu_uv.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static const GpuRenderBackend SW_BACKEND = {
|
||||
@@ -65,6 +67,47 @@ extern const GpuRenderBackend *vk_backend_get(void);
|
||||
|
||||
static const GpuRenderBackend *g_b = &SW_BACKEND;
|
||||
static GrBackend g_effective = GR_BACKEND_SOFTWARE;
|
||||
static int g_hd_offset_x, g_hd_offset_y;
|
||||
static uint32_t g_hd_texture_window;
|
||||
static int g_hd_semi, g_hd_perspective;
|
||||
static void hd_observe_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,
|
||||
int x2,int y2,int u2,int v2,uint16_t cx,uint16_t cy,uint16_t tp) {
|
||||
if (g_effective == GR_BACKEND_OPENGL || !gpu_hd_textures_dump_enabled()) return;
|
||||
if (g_effective == GR_BACKEND_VULKAN) g_b->vram_read(0,0);
|
||||
int limits[4], xs[3]={x0,x1,x2},ys[3]={y0,y1,y2},us[3]={u0,u1,u2},vs[3]={v0,v1,v2};
|
||||
if (g_hd_perspective) psx_uv_tri_world_limits(us,vs,limits);
|
||||
else psx_uv_tri_limits(xs,ys,us,vs,limits);
|
||||
gpu_hd_textures_observe_draw(tp,cx,cy,limits,g_hd_texture_window,g_hd_semi);
|
||||
}
|
||||
static void hd_observe_rect(int u0,int v0,int u1,int v1,uint16_t cx,uint16_t cy,uint16_t tp) {
|
||||
if (g_effective == GR_BACKEND_OPENGL || !gpu_hd_textures_dump_enabled()) return;
|
||||
if (g_effective == GR_BACKEND_VULKAN) g_b->vram_read(0,0);
|
||||
int limits[4]; psx_uv_rect_limits(u0,v0,u1,v1,limits);
|
||||
gpu_hd_textures_observe_draw(tp,cx,cy,limits,g_hd_texture_window,g_hd_semi);
|
||||
}
|
||||
|
||||
/* Residency describes guest native words, so any draw/copy which can touch
|
||||
* an upload ends its identity. Run after submission: an overlapping texture
|
||||
* draw must resolve its source before the destination invalidates it. */
|
||||
static void hd_invalidate_draw(int x0, int y0, int x1, int y1) {
|
||||
int ax0, ay0, ax1, ay1;
|
||||
if (!gpu_hd_textures_active()) return;
|
||||
g_b->get_draw_area(&ax0, &ay0, &ax1, &ay1);
|
||||
x0 += g_hd_offset_x; x1 += g_hd_offset_x;
|
||||
y0 += g_hd_offset_y; y1 += g_hd_offset_y;
|
||||
if (x0 < ax0) x0 = ax0; if (x1 > ax1) x1 = ax1;
|
||||
if (y0 < ay0) y0 = ay0; if (y1 > ay1) y1 = ay1;
|
||||
if (x0 <= x1 && y0 <= y1)
|
||||
gpu_hd_textures_invalidate(x0, y0, x1 - x0 + 1, y1 - y0 + 1);
|
||||
}
|
||||
static void hd_invalidate_triangle(int x0, int y0, int x1, int y1, int x2, int y2) {
|
||||
int xmin = x0, xmax = x0, ymin = y0, ymax = y0;
|
||||
if (x1 < xmin) xmin = x1; if (x2 < xmin) xmin = x2;
|
||||
if (x1 > xmax) xmax = x1; if (x2 > xmax) xmax = x2;
|
||||
if (y1 < ymin) ymin = y1; if (y2 < ymin) ymin = y2;
|
||||
if (y1 > ymax) ymax = y1; if (y2 > ymax) ymax = y2;
|
||||
hd_invalidate_draw(xmin, ymin, xmax, ymax);
|
||||
}
|
||||
|
||||
void gr_set_backend(GrBackend backend) {
|
||||
if (backend == GR_BACKEND_OPENGL) {
|
||||
@@ -95,14 +138,14 @@ void gr_set_backend(GrBackend backend) {
|
||||
GrBackend gr_backend(void) { return g_effective; }
|
||||
|
||||
/* ---- Dispatch wrappers (one line each; forward to the active backend) ---- */
|
||||
void gr_init(uint16_t *vram) { g_b->init(vram); }
|
||||
void gr_init(uint16_t *vram) { g_b->init(vram); gpu_hd_textures_set_vram(vram); g_hd_offset_x = g_hd_offset_y = 0; g_hd_texture_window = 0; g_hd_semi = g_hd_perspective = 0; }
|
||||
void gr_set_scale(int scale) { g_b->set_scale(scale); }
|
||||
int gr_scale(void) { return g_b->scale(); }
|
||||
void gr_set_texture_filter(int bilinear) { g_b->set_texture_filter(bilinear); }
|
||||
int gr_texture_filter(void) { return g_b->texture_filter(); }
|
||||
void gr_set_semi_transparency(int e, int m) { g_b->set_semi_transparency(e, m); }
|
||||
void gr_set_semi_transparency(int e, int m) { g_b->set_semi_transparency(e, m); g_hd_semi=e; }
|
||||
void gr_set_mask_bits(int s, int c) { g_b->set_mask_bits(s, c); }
|
||||
void gr_set_texture_window(uint32_t raw) { g_b->set_texture_window(raw); }
|
||||
void gr_set_texture_window(uint32_t raw) { g_b->set_texture_window(raw); g_hd_texture_window=raw; }
|
||||
void gr_set_color_modulation(int r, int g, int b, int raw) { g_b->set_color_modulation(r, g, b, raw); }
|
||||
void gr_set_precise_triangle(int enabled, int32_t x0, int32_t y0, int32_t x1, int32_t y1,
|
||||
int32_t x2, int32_t y2) {
|
||||
@@ -110,44 +153,60 @@ void gr_set_precise_triangle(int enabled, int32_t x0, int32_t y0, int32_t x1, in
|
||||
g_b->set_precise_triangle(enabled, x0, y0, x1, y1, x2, y2);
|
||||
}
|
||||
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
|
||||
g_hd_perspective = enabled && q0 > 0 && q1 > 0 && q2 > 0;
|
||||
if (g_b->set_perspective_triangle)
|
||||
g_b->set_perspective_triangle(enabled, q0, q1, q2);
|
||||
}
|
||||
void gr_fill_rect(int x, int y, int w, int h, uint16_t c) { g_b->fill_rect(x, y, w, h, c); }
|
||||
void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h) { g_b->copy_rect(sx, sy, dx, dy, w, h); }
|
||||
void gr_fill_rect(int x, int y, int w, int h, uint16_t c) { g_b->fill_rect(x, y, w, h, c); gpu_hd_textures_invalidate(x, y, w, h); }
|
||||
void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h) { g_b->copy_rect(sx, sy, dx, dy, w, h); gpu_hd_textures_invalidate(dx, dy, w, h); }
|
||||
void gr_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, uint16_t c) {
|
||||
g_b->draw_flat_triangle(x0, y0, x1, y1, x2, y2, c);
|
||||
hd_invalidate_triangle(x0, y0, x1, y1, x2, y2);
|
||||
g_hd_perspective = 0;
|
||||
}
|
||||
void gr_draw_gouraud_triangle(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1,
|
||||
int x2, int y2, uint16_t c2) {
|
||||
g_b->draw_gouraud_triangle(x0, y0, c0, x1, y1, c1, x2, y2, c2);
|
||||
hd_invalidate_triangle(x0, y0, x1, y1, x2, y2);
|
||||
g_hd_perspective = 0;
|
||||
}
|
||||
void gr_draw_textured_triangle(int x0, int y0, int u0, int v0, int x1, int y1, int u1, int v1,
|
||||
int x2, int y2, int u2, int v2,
|
||||
uint16_t clut_x, uint16_t clut_y, uint16_t texpage) {
|
||||
hd_observe_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,clut_x,clut_y,texpage);
|
||||
g_b->draw_textured_triangle(x0, y0, u0, v0, x1, y1, u1, v1, x2, y2, u2, v2,
|
||||
clut_x, clut_y, texpage);
|
||||
hd_invalidate_triangle(x0, y0, x1, y1, x2, y2);
|
||||
g_hd_perspective = 0;
|
||||
}
|
||||
void gr_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0, uint32_t c0,
|
||||
int x1, int y1, int u1, int v1, uint32_t c1,
|
||||
int x2, int y2, int u2, int v2, uint32_t c2,
|
||||
uint16_t clut_x, uint16_t clut_y,
|
||||
uint16_t texpage, int raw) {
|
||||
hd_observe_triangle(x0,y0,u0,v0,x1,y1,u1,v1,x2,y2,u2,v2,clut_x,clut_y,texpage);
|
||||
g_b->draw_shaded_textured_triangle(x0, y0, u0, v0, c0, x1, y1, u1, v1, c1,
|
||||
x2, y2, u2, v2, c2, clut_x, clut_y, texpage, raw);
|
||||
hd_invalidate_triangle(x0, y0, x1, y1, x2, y2);
|
||||
g_hd_perspective = 0;
|
||||
}
|
||||
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t c) { g_b->draw_flat_rect(x, y, w, h, c); }
|
||||
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t c) { g_b->draw_flat_rect(x, y, w, h, c); if (w > 0 && h > 0) hd_invalidate_draw(x,y,x+w-1,y+h-1); }
|
||||
void gr_draw_textured_rect(int x, int y, int w, int h, int u, int v,
|
||||
uint16_t clut_x, uint16_t clut_y, uint16_t texpage) {
|
||||
hd_observe_rect(u,v,u+w,v+h,clut_x,clut_y,texpage);
|
||||
g_b->draw_textured_rect(x, y, w, h, u, v, clut_x, clut_y, texpage);
|
||||
if (w > 0 && h > 0) hd_invalidate_draw(x,y,x+w-1,y+h-1);
|
||||
}
|
||||
void gr_draw_textured_rect_scaled(int x, int y, int w, int h, int u0, int v0, int u1, int v1,
|
||||
uint16_t clut_x, uint16_t clut_y, uint16_t texpage) {
|
||||
hd_observe_rect(u0,v0,u1,v1,clut_x,clut_y,texpage);
|
||||
g_b->draw_textured_rect_scaled(x, y, w, h, u0, v0, u1, v1, clut_x, clut_y, texpage);
|
||||
if (w > 0 && h > 0) hd_invalidate_draw(x,y,x+w-1,y+h-1);
|
||||
}
|
||||
void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t c) { g_b->draw_line(x0, y0, x1, y1, c); }
|
||||
void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t c) { g_b->draw_line(x0, y0, x1, y1, c); hd_invalidate_triangle(x0,y0,x1,y1,x1,y1); }
|
||||
void gr_draw_shaded_line(int x0, int y0, uint16_t c0, int x1, int y1, uint16_t c1) {
|
||||
g_b->draw_shaded_line(x0, y0, c0, x1, y1, c1);
|
||||
hd_invalidate_triangle(x0,y0,x1,y1,x1,y1);
|
||||
}
|
||||
int gr_render_display(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||||
return g_b->render_display(o, p, dx, dy, dw, dh);
|
||||
@@ -155,13 +214,14 @@ int gr_render_display(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||||
int gr_render_display_hires(uint32_t *o, int p, int dx, int dy, int dw, int dh) {
|
||||
return g_b->render_display_hires(o, p, dx, dy, dw, dh);
|
||||
}
|
||||
void gr_vram_write(int x, int y, uint16_t pixel) { g_b->vram_write(x, y, pixel); }
|
||||
void gr_vram_upload_begin(int x, int y, int w, int h) { gpu_hd_textures_invalidate(x,y,w,h); }
|
||||
void gr_vram_write(int x, int y, uint16_t pixel) { g_b->vram_write(x, y, pixel); gpu_hd_textures_invalidate(x,y,1,1); }
|
||||
uint16_t gr_vram_read(int x, int y) { return g_b->vram_read(x, y); }
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *d) { g_b->vram_transfer_in(x, y, w, h, d); }
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *d) { g_b->vram_transfer_in(x, y, w, h, d); gpu_hd_textures_track_upload(x,y,w,h,d); }
|
||||
void gr_vram_transfer_out(int x, int y, int w, int h, uint16_t *d) { g_b->vram_transfer_out(x, y, w, h, d); }
|
||||
void gr_set_draw_area(int x1, int y1, int x2, int y2){ g_b->set_draw_area(x1, y1, x2, y2); }
|
||||
void gr_get_draw_area(int *x1, int *y1, int *x2, int *y2) { g_b->get_draw_area(x1, y1, x2, y2); }
|
||||
void gr_set_draw_offset(int x, int y) { g_b->set_draw_offset(x, y); }
|
||||
void gr_set_draw_offset(int x, int y) { g_b->set_draw_offset(x, y); g_hd_offset_x=x; g_hd_offset_y=y; }
|
||||
|
||||
/* Native-wide compositor — present only on backends that supply it. */
|
||||
int gr_wide_supported(void) { return g_b->render_wide_display != 0; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -774,7 +774,7 @@ std::string canonical_resolution(const std::vector<const ModPackage*>& ordered,
|
||||
out << resource.format << ':' << resource.bytes->size() << ':'
|
||||
<< resource.sha256;
|
||||
else
|
||||
out << resource.path.string();
|
||||
out << resource.path.u8string();
|
||||
out << '\n';
|
||||
}
|
||||
return out.str();
|
||||
@@ -870,7 +870,7 @@ fs::path effective_resource_path(const ModPackage& package,
|
||||
if (!selected) return {};
|
||||
const auto resource = selected->resources.find(id);
|
||||
return resource == selected->resources.end() ? fs::path{} :
|
||||
fs::path(resource->second);
|
||||
fs::u8path(resource->second);
|
||||
}
|
||||
|
||||
bool prospective_feature_enabled(
|
||||
@@ -3206,7 +3206,7 @@ bool ModPackageManager::set_feature_resource_path(
|
||||
return false;
|
||||
}
|
||||
if (!resource->shared_source.empty()) sources_[resource->shared_source] = path;
|
||||
else selections_[package_id].features[feature_id].resources[resource_id] = path.string();
|
||||
else selections_[package_id].features[feature_id].resources[resource_id] = path.u8string();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3458,7 +3458,7 @@ bool ModPackageManager::prepare_resources(const std::string& game_id,
|
||||
const auto* resource = find_resource(*package, feature.id, name);
|
||||
if (!resource || resource->input_only || resource->sha256.empty())
|
||||
throw std::runtime_error("preparer returned an undeclared or unverified output");
|
||||
pending[id].features[feature.id].resources[name] = path.string();
|
||||
pending[id].features[feature.id].resources[name] = path.u8string();
|
||||
}
|
||||
for (const auto& r : package->resources)
|
||||
if (r.feature_id == feature.id && !r.input_only && r.required && !outputs.count(r.id))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "mod_packages.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "psx_memory.h"
|
||||
#include "render_pass_projection.h"
|
||||
#include "psx_sha256.h"
|
||||
@@ -1021,7 +1022,7 @@ int provider_feature_resource_get(void*, const char* package_id,
|
||||
copy_text(out->label, sizeof(out->label), resource.label);
|
||||
copy_text(out->description, sizeof(out->description),
|
||||
resource.description);
|
||||
copy_text(out->path, sizeof(out->path), path.string());
|
||||
copy_text(out->path, sizeof(out->path), path.u8string());
|
||||
copy_text(out->status, sizeof(out->status),
|
||||
path.empty() ? "Not selected" :
|
||||
(verified ? "Selected" : "Selected path is missing"));
|
||||
@@ -1045,7 +1046,7 @@ int provider_feature_resource_set_path(void*, const char* package_id,
|
||||
return mutate([&](std::string& error) {
|
||||
return state().manager.set_feature_resource_path(
|
||||
package_id, feature_id, resource_id,
|
||||
std::filesystem::path(path), &error);
|
||||
std::filesystem::u8path(path), &error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1287,6 +1288,43 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
|
||||
"psxrecomp: mod selection kept but inactive: %s is not "
|
||||
"in this build's mod catalog\n",
|
||||
dormant.c_str());
|
||||
/* psx.hd-textures has a shared directory-resource contract, independent of
|
||||
* the title package id. Keep owner files outside the build-owned bundled
|
||||
* tree, and give the launcher a useful Open folder action on first use. */
|
||||
std::string texture_game_id = game_id;
|
||||
for (char& c : texture_game_id)
|
||||
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-' && c != '_')
|
||||
c = '_';
|
||||
if (texture_game_id.empty()) texture_game_id = "game";
|
||||
for (const auto& entry : s.manager.packages()) {
|
||||
const ModPackage* package = s.manager.selected_package(entry.first);
|
||||
if (!package || !std::any_of(package->targets.begin(), package->targets.end(),
|
||||
[&](const ModTarget& target) {
|
||||
return target.game_id == game_id || target.game_id == "*";
|
||||
})) continue;
|
||||
for (const ModPlugin& plugin : package->plugins) {
|
||||
if (plugin.id != "psx.hd-textures" ||
|
||||
!s.manager.feature_resource_path(package->id, plugin.feature_id, "pack").empty())
|
||||
continue;
|
||||
const auto resource = std::find_if(package->resources.begin(), package->resources.end(),
|
||||
[&](const ModResource& r) {
|
||||
return r.feature_id == plugin.feature_id && r.id == "pack" &&
|
||||
(r.format == "directory" || r.format == "folder");
|
||||
});
|
||||
if (resource == package->resources.end()) continue;
|
||||
std::error_code ec;
|
||||
const auto pack = std::filesystem::absolute(
|
||||
root / "texture-packs" / texture_game_id, ec);
|
||||
if (!ec) std::filesystem::create_directories(pack / "replacements", ec);
|
||||
if (!ec) std::filesystem::create_directories(pack / "dumps", ec);
|
||||
std::string directory_error;
|
||||
if (ec || !s.manager.set_feature_resource_path(package->id,
|
||||
plugin.feature_id, "pack", pack, &directory_error)) {
|
||||
std::fprintf(stderr, "psxrecomp: texture pack folder: %s\n",
|
||||
ec ? ec.message().c_str() : directory_error.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!sha256_file(exe_path, s.exe_sha256, &s.error)) {
|
||||
/* Release installs commonly do not carry a loose PS-X EXE; game-id and
|
||||
* expected-byte guards remain available in that case. */
|
||||
@@ -2129,6 +2167,7 @@ extern "C" void mod_runtime_activate_plugins(void) {
|
||||
psx_projection_reset_session();
|
||||
gpu_ws_set_native_scene_predicate(nullptr);
|
||||
psx_ram_reset_size_request();
|
||||
gpu_hd_textures_shutdown();
|
||||
if (!s.initialized || !s.plan.ok) return;
|
||||
s.disc_extents.clear();
|
||||
s.audio_tracks.clear();
|
||||
@@ -2194,7 +2233,7 @@ extern "C" int psx_mod_current_resource_path(const char* resource_id,
|
||||
resource.feature_id != s.current_plugin->feature_id ||
|
||||
resource.id != resource_id)
|
||||
continue;
|
||||
const std::string text = resource.path.string();
|
||||
const std::string text = resource.path.u8string();
|
||||
if (text.empty() || text.size() + 1 > (size_t)out_size) return 0;
|
||||
std::memcpy(out, text.c_str(), text.size() + 1);
|
||||
return 1;
|
||||
@@ -2202,6 +2241,49 @@ extern "C" int psx_mod_current_resource_path(const char* resource_id,
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_current_option_value(const char* option_id,
|
||||
char* out, uint32_t out_size) {
|
||||
using namespace PSXRecompV4;
|
||||
if (out && out_size) out[0] = '\0';
|
||||
const auto* plugin = state().current_plugin;
|
||||
if (!plugin) return 0;
|
||||
return psx_mod_option_value(plugin->package_id.c_str(), plugin->feature_id.c_str(),
|
||||
option_id, out, out_size);
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_set_hd_texture_pack(const char* resource_id,
|
||||
int replacements_enabled, int dump_enabled) {
|
||||
char root[4096] = "";
|
||||
char error[512] = "";
|
||||
if (!psx_mod_current_resource_path(resource_id, root, sizeof(root))) {
|
||||
std::fprintf(stderr, "psxrecomp: HD textures require a selected pack folder\n");
|
||||
return 0;
|
||||
}
|
||||
if (!gpu_hd_textures_configure(root, replacements_enabled, dump_enabled,
|
||||
error, sizeof(error))) {
|
||||
std::fprintf(stderr, "psxrecomp: HD textures: %s\n", error);
|
||||
return 0;
|
||||
}
|
||||
std::fprintf(stdout, "psxrecomp: HD textures: %s (replacements %s, dumping %s)\n",
|
||||
root, replacements_enabled ? "on" : "off", dump_enabled ? "on" : "off");
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_set_hd_texture_dump(int enabled) {
|
||||
GpuHdTextureDiag info{};
|
||||
gpu_hd_textures_get_diag(&info);
|
||||
if (!info.root || !info.root[0]) return 0;
|
||||
gpu_hd_textures_set_dump_enabled(enabled);
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_reload_hd_texture_pack(void) {
|
||||
char error[512] = "";
|
||||
const int ok = gpu_hd_textures_reload(error, sizeof(error));
|
||||
if (!ok) std::fprintf(stderr, "psxrecomp: HD textures: %s\n", error);
|
||||
return ok;
|
||||
}
|
||||
|
||||
extern "C" uint8_t psx_mod_read_byte(uint32_t address) {
|
||||
return psx_read_byte(address);
|
||||
}
|
||||
|
||||
+18
-7
@@ -55,8 +55,10 @@ static void png_chunk(FILE *f, const char *type, const uint8_t *data, size_t len
|
||||
if (len) fwrite(data, 1, len, f);
|
||||
png_put_be32(f, crc ^ 0xFFFFFFFFu);
|
||||
}
|
||||
/* Write an RGB (3 bytes/pixel, top-down) buffer as a PNG. Returns 1 on success. */
|
||||
static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||||
/* Write top-down RGB/RGBA data. This internal helper has static linkage so
|
||||
* the public wrappers stay usable by the existing C debug servers. */
|
||||
static int png_write_channels(FILE *f, const uint8_t *pixels, uint32_t w,
|
||||
uint32_t h, unsigned channels) {
|
||||
static const uint8_t sig[8] = { 137,80,78,71,13,10,26,10 };
|
||||
fwrite(sig, 1, 8, f);
|
||||
|
||||
@@ -64,20 +66,20 @@ static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||||
ihdr[0]=(uint8_t)(w>>24); ihdr[1]=(uint8_t)(w>>16); ihdr[2]=(uint8_t)(w>>8); ihdr[3]=(uint8_t)w;
|
||||
ihdr[4]=(uint8_t)(h>>24); ihdr[5]=(uint8_t)(h>>16); ihdr[6]=(uint8_t)(h>>8); ihdr[7]=(uint8_t)h;
|
||||
ihdr[8]=8; /* bit depth */
|
||||
ihdr[9]=2; /* color type 2 = truecolor RGB */
|
||||
ihdr[9]=(uint8_t)(channels == 4 ? 6 : 2); /* RGBA or RGB */
|
||||
ihdr[10]=0; /* compression */
|
||||
ihdr[11]=0; /* filter */
|
||||
ihdr[12]=0; /* interlace */
|
||||
png_chunk(f, "IHDR", ihdr, sizeof ihdr);
|
||||
|
||||
/* Filtered raw scanlines: each row prefixed with filter byte 0 (None). */
|
||||
size_t raw_len = (size_t)h * (1 + (size_t)w * 3);
|
||||
size_t raw_len = (size_t)h * (1 + (size_t)w * channels);
|
||||
uint8_t *raw = (uint8_t *)malloc(raw_len);
|
||||
if (!raw) return 0;
|
||||
for (uint32_t y = 0; y < h; y++) {
|
||||
uint8_t *row = raw + (size_t)y * (1 + (size_t)w * 3);
|
||||
uint8_t *row = raw + (size_t)y * (1 + (size_t)w * channels);
|
||||
row[0] = 0;
|
||||
memcpy(row + 1, rgb + (size_t)y * w * 3, (size_t)w * 3);
|
||||
memcpy(row + 1, pixels + (size_t)y * w * channels, (size_t)w * channels);
|
||||
}
|
||||
|
||||
/* zlib stream: 2-byte header + stored DEFLATE blocks + 4-byte Adler32. */
|
||||
@@ -106,7 +108,16 @@ static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||||
png_chunk(f, "IDAT", z, zi);
|
||||
free(z);
|
||||
png_chunk(f, "IEND", NULL, 0);
|
||||
return 1;
|
||||
return !ferror(f);
|
||||
}
|
||||
|
||||
/* Write an RGB (3 bytes/pixel, top-down) buffer as a PNG. Returns 1 on success. */
|
||||
static int png_write_rgb(FILE *f, const uint8_t *rgb, uint32_t w, uint32_t h) {
|
||||
return png_write_channels(f, rgb, w, h, 3);
|
||||
}
|
||||
/* Preserve raw 8-bit RGBA, including DuckStation's ST alpha convention. */
|
||||
static int png_write_rgba(FILE *f, const uint8_t *rgba, uint32_t w, uint32_t h) {
|
||||
return png_write_channels(f, rgba, w, h, 4);
|
||||
}
|
||||
|
||||
#endif /* PSX_PNG_WRITE_H */
|
||||
|
||||
@@ -357,6 +357,7 @@ int gr_render_display_hires(uint32_t *out, int pitch, int dx, int dy, int dw, in
|
||||
(void)out; (void)pitch; (void)dx; (void)dy; (void)dw; (void)dh;
|
||||
return 0;
|
||||
}
|
||||
void gr_vram_upload_begin(int x,int y,int w,int h) { (void)x; (void)y; (void)w; (void)h; }
|
||||
void gr_vram_write(int x, int y, uint16_t pixel) { (void)x; (void)y; (void)pixel; }
|
||||
uint16_t gr_vram_read(int x, int y) { (void)x; (void)y; return 0; }
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Existing GL fixtures deliberately run without any external pack. The HD
|
||||
* fixture links the real session adapter instead. */
|
||||
#ifndef PSX_TEST_HD_TEXTURE_PACK
|
||||
int gpu_hd_textures_acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy,
|
||||
const int limits[4], uint32_t window, int semi, GpuHdTextureImage* image) {
|
||||
(void)tp; (void)cx; (void)cy; (void)limits; (void)window; (void)semi;
|
||||
memset(image, 0, sizeof(*image)); return 0;
|
||||
}
|
||||
void gpu_hd_textures_release_image(GpuHdTextureImage* image) {
|
||||
memset(image, 0, sizeof(*image));
|
||||
}
|
||||
void gpu_hd_textures_note_applied(void) {}
|
||||
void gpu_hd_textures_reset_tracking(void) {}
|
||||
#endif
|
||||
@@ -170,3 +170,4 @@ void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *d) {
|
||||
(void)x; (void)y; memcpy(s_vram, d, (size_t)w * (size_t)h * 2u);
|
||||
g_stub_device_restores++;
|
||||
}
|
||||
void gr_vram_upload_begin(int x,int y,int w,int h) { (void)x; (void)y; (void)w; (void)h; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Compile and check palette-aware minification on a hidden real GL context."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import subprocess
|
||||
@@ -13,14 +14,19 @@ def main():
|
||||
parser.add_argument('--sdl-library',required=True)
|
||||
parser.add_argument('--output',required=True)
|
||||
parser.add_argument('--fixture',type=Path)
|
||||
parser.add_argument('--hd-pack', action='store_true')
|
||||
parser.add_argument('--cxx')
|
||||
parser.add_argument('--argument', action='append', default=[])
|
||||
args=parser.parse_args()
|
||||
fw=Path(__file__).resolve().parents[2]
|
||||
out=Path(args.output).resolve(); out.mkdir(parents=True,exist_ok=True)
|
||||
receipt=[]
|
||||
def run(command):
|
||||
def run(command,environment=None):
|
||||
command=[str(x) for x in command]
|
||||
r=subprocess.run(command,cwd=out,capture_output=True,text=True,encoding='utf-8',errors='replace')
|
||||
receipt.append(dict(cmd=command,exit=r.returncode,stdout=r.stdout,stderr=r.stderr))
|
||||
env=os.environ.copy()
|
||||
if environment: env.update(environment)
|
||||
r=subprocess.run(command,cwd=out,capture_output=True,text=True,encoding='utf-8',errors='replace',env=env)
|
||||
receipt.append(dict(cmd=command,env=environment,exit=r.returncode,stdout=r.stdout,stderr=r.stderr))
|
||||
(out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n',encoding='utf-8')
|
||||
print(r.stdout,r.stderr[-4000:],flush=True)
|
||||
if r.returncode: raise subprocess.CalledProcessError(r.returncode,command)
|
||||
@@ -28,7 +34,7 @@ def main():
|
||||
'-ffunction-sections','-fdata-sections','-I',fw/'runtime/include','-I',fw/'runtime/src']
|
||||
for include in args.sdl_include.split(';'):
|
||||
if include: flags+=['-I',include]
|
||||
sources=[('probe',args.fixture or fw/'runtime/tests/test_gl_texture_filter.c'),
|
||||
sources=[('probe',args.fixture.resolve() if args.fixture else fw/'runtime/tests/test_gl_texture_filter.c'),
|
||||
('sw',fw/'runtime/src/gpu_sw_renderer.c'),('fi',fw/'runtime/src/frame_interpolation.c'),
|
||||
('rp',fw/'runtime/src/render_pass_plan.c')]
|
||||
if (fw/'runtime/src/psx_openxr.c').exists():
|
||||
@@ -36,6 +42,13 @@ def main():
|
||||
objects=[]
|
||||
for name,source in sources:
|
||||
obj=out/(name+'.o'); run([args.cc,*flags,'-c',source,'-o',obj]); objects.append(obj)
|
||||
if args.hd_pack:
|
||||
if not args.cxx: parser.error('--hd-pack requires --cxx')
|
||||
cppflags=[arg if arg!='-std=gnu11' else '-std=c++17' for arg in flags]
|
||||
for name in ('gpu_hd_textures','hd_texture_pack','duckstation_texture_pack'):
|
||||
obj=out/(name+'.o')
|
||||
run([args.cxx,*cppflags,'-c',fw/'runtime/src'/(name+'.cpp'),'-o',obj])
|
||||
objects.append(obj)
|
||||
exe=out/('filter.exe' if platform.system()=='Windows' else 'filter')
|
||||
libraries=['-lm']
|
||||
if platform.system()=='Windows':
|
||||
@@ -47,8 +60,15 @@ def main():
|
||||
libraries+=['-liconv','-Wl,-dead_strip']
|
||||
else: libraries+=['-lGL','-ldl','-lpthread']
|
||||
if platform.system()!='Darwin': libraries+=['-Wl,--gc-sections']
|
||||
run([args.cc,*objects,args.sdl_library,'-o',exe,*libraries])
|
||||
run([exe])
|
||||
run([args.cxx if args.hd_pack else args.cc,*objects,args.sdl_library,'-o',exe,*libraries])
|
||||
arguments=list(args.argument)
|
||||
if args.hd_pack:
|
||||
pack=out/'pack'; (pack/'replacements').mkdir(parents=True,exist_ok=True)
|
||||
(pack/'beetle'/'demo-texture-replacements').mkdir(parents=True,exist_ok=True)
|
||||
arguments=[pack]
|
||||
run([exe,*arguments],{'PSX_GL_HIRES_WINDOW':'0'} if args.hd_pack else None)
|
||||
if args.hd_pack: run([exe,*arguments],{'PSX_GL_HIRES_WINDOW':'1'})
|
||||
if args.hd_pack: run([exe,'--native-baseline'],{'PSX_GL_HIRES_WINDOW':'1'})
|
||||
return 0
|
||||
|
||||
if __name__=='__main__':
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "../third_party/stb_image.h"
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "../src/png_write.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
namespace {
|
||||
int failures = 0;
|
||||
void check(bool ok, const char* what) {
|
||||
if (!ok) { std::fprintf(stderr,"FAIL: %s\n",what); ++failures; }
|
||||
}
|
||||
using Pack = std::unique_ptr<DuckTexturePack,decltype(&duck_texture_pack_destroy)>;
|
||||
Pack create(const fs::path& root) {
|
||||
DuckTexturePack* pack = nullptr; char error[512]{};
|
||||
check(duck_texture_pack_create(root.u8string().c_str(),&pack,error,sizeof(error)) == 1,error);
|
||||
return Pack(pack,&duck_texture_pack_destroy);
|
||||
}
|
||||
std::vector<uint16_t> words(size_t n) {
|
||||
std::vector<uint16_t> result(n);
|
||||
for (size_t i = 0; i < n; ++i) result[i] = static_cast<uint16_t>((i*7919)^0xa51c);
|
||||
return result;
|
||||
}
|
||||
void place(std::vector<uint16_t>& vram, unsigned x, unsigned y, unsigned w,
|
||||
const std::vector<uint16_t>& source) {
|
||||
for (size_t i = 0; i < source.size(); ++i) vram[(y+i/w)*1024+x+i%w] = source[i];
|
||||
}
|
||||
HdTextureDrawQuery query(std::vector<uint16_t>& vram, uint16_t page_x, uint16_t page_y,
|
||||
uint8_t depth, uint8_t u0, uint8_t u1, uint8_t v0, uint8_t v1,
|
||||
uint16_t clut_x = 512, uint16_t clut_y = 400) {
|
||||
return {page_x,page_y,depth,u0,u1,v0,v1,clut_x,clut_y,vram.data(),vram.size()};
|
||||
}
|
||||
void png(const fs::path& path, unsigned w = 4, unsigned h = 4) {
|
||||
fs::create_directories(path.parent_path());
|
||||
std::vector<uint8_t> pixels(size_t(w)*h*4);
|
||||
constexpr uint8_t alpha[] = {0,127,128,242,243,255};
|
||||
for (size_t i = 0; i < size_t(w)*h; ++i) {
|
||||
pixels[i*4] = 23; pixels[i*4+1] = 57; pixels[i*4+2] = 91; pixels[i*4+3] = alpha[i%6];
|
||||
}
|
||||
#ifdef _WIN32
|
||||
FILE* file = _wfopen(path.c_str(),L"wb");
|
||||
#else
|
||||
FILE* file = std::fopen(path.c_str(),"wb");
|
||||
#endif
|
||||
check(file != nullptr,"open original test PNG");
|
||||
if (file) { check(png_write_rgba(file,pixels.data(),w,h) == 1,"encode original test PNG"); std::fclose(file); }
|
||||
}
|
||||
DuckTexturePixels decoded(DuckTexturePack* pack, uint64_t id) {
|
||||
DuckTexturePixels pixels{};
|
||||
check(duck_texture_pack_request_decode(pack,id) >= 0,"queue PNG decode");
|
||||
const auto end = std::chrono::steady_clock::now()+std::chrono::seconds(5);
|
||||
int status = HD_TEXTURE_LOOKUP_NONE;
|
||||
do {
|
||||
status = duck_texture_pack_acquire_decoded(pack,id,&pixels);
|
||||
if (status != HD_TEXTURE_LOOKUP_NONE) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
} while (std::chrono::steady_clock::now() < end);
|
||||
check(status == HD_TEXTURE_LOOKUP_FOUND,"background PNG decode becomes ready");
|
||||
return pixels;
|
||||
}
|
||||
bool decode_fails(DuckTexturePack* pack, uint64_t id) {
|
||||
int status = duck_texture_pack_request_decode(pack,id);
|
||||
const auto end = std::chrono::steady_clock::now()+std::chrono::seconds(5);
|
||||
while (status == HD_TEXTURE_LOOKUP_NONE && std::chrono::steady_clock::now() < end) {
|
||||
DuckTexturePixels pixels{};
|
||||
status = duck_texture_pack_acquire_decoded(pack,id,&pixels);
|
||||
duck_texture_pixels_release(&pixels);
|
||||
if (status == HD_TEXTURE_LOOKUP_NONE) std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
return status == HD_TEXTURE_LOOKUP_ERROR;
|
||||
}
|
||||
|
||||
void fixed_hash_vectors() {
|
||||
/* Independently produced by Python xxhash 3.6.0's native wheel. These
|
||||
* literal expectations cover XXH3's short/medium/long-input boundaries. */
|
||||
const std::pair<size_t,uint64_t> vectors[] = {
|
||||
{0,0x2d06800538d394c2ULL},{1,0xc5779b2f56fbe2e1ULL},{2,0x0edb68960f55aef6ULL},
|
||||
{3,0xfce9d54aec3f9d32ULL},{4,0x3631d2a04ec85311ULL},{8,0x000ca76cb9b329dcULL},
|
||||
{9,0xab6a9091cf532d8aULL},{16,0x9cbcdea9305d7528ULL},{17,0x2f133c3e193cbe48ULL},
|
||||
{32,0x3e4a087da5cd5990ULL},{64,0xc3e3ef9ad7f08dacULL},{65,0x740c4e7d1690d606ULL},
|
||||
{120,0x5942d561231419b2ULL},{121,0x152cb5857c4535f3ULL},{128,0x351cd56a8fe50e66ULL},
|
||||
{129,0x38a45de7ae84906eULL},{256,0x0005d0b83a005f4eULL},{8192,0xc68a00dcca652920ULL}
|
||||
};
|
||||
for (const auto& v : vectors) {
|
||||
const auto input = words(v.first);
|
||||
check(duck_texture_hash_words_le(input.data(),input.size()) == v.second,"fixed independent LE16 XXH3 vector");
|
||||
}
|
||||
std::vector<uint16_t> vram(1024*512,0xf00d);
|
||||
place(vram,101,39,2,words(8));
|
||||
check(duck_texture_hash_rect(vram.data(),vram.size(),101,39,2,4) == 0x000ca76cb9b329dcULL,
|
||||
"strided native rectangle excludes unrelated VRAM words");
|
||||
check(duck_texture_hash_rect(vram.data(),vram.size(),1023,0,2,1) == 0,"wrapped hash rectangle rejected");
|
||||
}
|
||||
|
||||
void names() {
|
||||
DuckTextureKey key{};
|
||||
check(duck_texture_parse_name("texupload-P4-0123456789abcdef-FEDCBA9876543210-64x256-0-192-64x64-P0-14.png",&key) == 1,
|
||||
"literal upstream documented upload filename");
|
||||
check(key.source_hash == 0x0123456789abcdefULL && key.palette_hash == 0xfedcba9876543210ULL &&
|
||||
key.source_width_words == 64 && key.offset_y == 192 && key.width == 64 && key.palette_max == 14,
|
||||
"filename dimensions distinguish source words from expanded texels");
|
||||
check(duck_texture_parse_name("texpage-STC16-0123456789ABCDEF-256x256-8-16-32x64.PNG",&key) == 1 &&
|
||||
key.kind == DUCK_TEXTURE_PAGE && key.semitransparent && key.depth == 2 && !key.palette_hash,
|
||||
"direct ST filename omits palette fields");
|
||||
const char* invalid[] = {
|
||||
"texpage-P4-12345678-0123456789ABCDEF-64x256-0-0-4x1-P0-15.png",
|
||||
"texpage-P4-0123456789ABCDEF-0123456789ABCDEF-64x256-0-0-3x1-P0-15.png",
|
||||
"texpage-P4-0123456789ABCDEF-0123456789ABCDEF-64x256-0-0-4x1-P0-16.png",
|
||||
"texpage-P8-0123456789ABCDEF-0123456789ABCDEF-128x256-0-0-4x1-P15-0.png",
|
||||
"texupload-C16-0123456789ABCDEF-0x256-0-0-4x1.png",
|
||||
"texupload-C16-0123456789ABCDEF-65536x1-0-0-4x1.png",
|
||||
"texpage-C16-0123456789ABCDEF-256x256-255-0-2x1.png",
|
||||
"texpage-C16-0123456789ABCDEF-256x256-0-0-4x1.png.old",
|
||||
"../texpage-C16-0123456789ABCDEF-256x256-0-0-4x1.png"
|
||||
};
|
||||
for (const char* name : invalid) check(!duck_texture_parse_name(name,&key),"targeted malformed/unsupported name rejected");
|
||||
}
|
||||
|
||||
void page_and_decode(const fs::path& root) {
|
||||
const char* name = "texpage-P4-3631D2A04EC85311-9CBCDEA9305D7528-64x256-4-2-8x2-P0-15.png";
|
||||
png(root/"replacements"/"nested"/name,16,4);
|
||||
std::vector<uint16_t> vram(1024*512);
|
||||
place(vram,65,258,2,words(4)); place(vram,512,400,16,words(16));
|
||||
auto pack = create(root); auto q = query(vram,64,256,0,5,10,2,3);
|
||||
DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"fixed subpage fixture matches");
|
||||
check(match.origin_u == 4 && match.origin_v == 2 && match.source_width == 8 && match.source_height == 2,
|
||||
"crop extent and origin are page-space texels");
|
||||
auto pixels = decoded(pack.get(),match.entry_id);
|
||||
check(pixels.width == 16 && pixels.height == 4,"arbitrary PNG scaling uses image dimensions");
|
||||
constexpr uint8_t alpha[] = {0,127,128,242,243,255};
|
||||
if (pixels.rgba) for (unsigned i = 0; i < 6; ++i)
|
||||
check(pixels.rgba[i*4+3] == alpha[i],"decode retains raw alpha boundary bytes");
|
||||
const uint16_t old = vram[258*1024+65];
|
||||
vram[258*1024+65] ^= 1; duck_texture_pack_invalidate(pack.get(),65,258,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"write invalidates positive page cache");
|
||||
vram[258*1024+65] = old; duck_texture_pack_invalidate(pack.get(),65,258,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"write invalidates negative page cache");
|
||||
vram[400*1024+512] ^= 1; duck_texture_pack_invalidate(pack.get(),512,400,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"CLUT write invalidates cached match");
|
||||
pack.reset();
|
||||
check(pixels.rgba && pixels.rgba[0] == 23,"pixel lease outlives pack and decode worker");
|
||||
duck_texture_pixels_release(&pixels);
|
||||
}
|
||||
|
||||
void upload(const fs::path& root) {
|
||||
png(root/"replacements"/"texupload-C16-000CA76CB9B329DC-4x2-2-0-2x2.png",4,8);
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,66,257,4,words(8));
|
||||
auto pack = create(root); auto q = query(vram,64,256,2,4,5,1,2);
|
||||
DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"upload names require original source identity");
|
||||
check(duck_texture_pack_track_upload(pack.get(),66,257,4,2,vram.data(),vram.size()),"track post-write native upload");
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND && match.origin_u == 4 && match.origin_v == 1,
|
||||
"upload crop has correct source origin");
|
||||
duck_texture_pack_invalidate(pack.get(),66,257,1,1); /* outside replacement crop */
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"partial source overwrite drops entire upload identity");
|
||||
duck_texture_pack_track_upload(pack.get(),66,257,4,2,vram.data(),vram.size());
|
||||
duck_texture_pack_reset_tracking(pack.get());
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"savestate/reset clears upload identity");
|
||||
check(!duck_texture_pack_track_upload(pack.get(),1023,0,2,1,vram.data(),vram.size()),"wrapped upload explicitly falls back");
|
||||
}
|
||||
|
||||
void reload_tracking(const fs::path& root) {
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,66,257,4,words(8));
|
||||
auto old = create(root);
|
||||
check(duck_texture_pack_track_upload(old.get(),66,257,4,2,vram.data(),vram.size()),"capture upload before replacement file exists");
|
||||
png(root/"replacements"/"texupload-C16-000CA76CB9B329DC-4x2-2-0-2x2.png");
|
||||
auto refreshed = create(root); auto q = query(vram,64,256,2,4,5,1,2); DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"newly indexed replacement initially has no upload identity");
|
||||
check(duck_texture_pack_copy_tracking(refreshed.get(),old.get()),"same-session reload copies upload tracking");
|
||||
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"copied tracking makes newly indexed replacement immediately match");
|
||||
duck_texture_pack_invalidate(refreshed.get(),66,257,1,1);
|
||||
check(duck_texture_pack_match(refreshed.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"destination tracking invalidates independently");
|
||||
auto second = create(root); duck_texture_pack_copy_tracking(second.get(),old.get());
|
||||
check(duck_texture_pack_match(second.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"destination invalidation leaves original tracker intact");
|
||||
}
|
||||
|
||||
void palettes(const fs::path& root) {
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,128,0,1,words(2));
|
||||
place(vram,1008,400,16,words(16));
|
||||
const char* edge = "texpage-P8-0EDB68960F55AEF6-9CBCDEA9305D7528-128x256-0-0-2x2-P0-255.png";
|
||||
png(root/"edge"/"replacements"/edge);
|
||||
auto pack = create(root/"edge"); auto q = query(vram,128,0,1,0,1,0,1,1008,400); DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"full P8 edge CLUT hashes remaining 16 words");
|
||||
vram[400*1024] = 42; duck_texture_pack_invalidate(pack.get(),0,400,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"full P8 CLUT hash does not wrap into row start");
|
||||
const char* partial = "texpage-P8-0EDB68960F55AEF6-3631D2A04EC85311-128x256-0-0-2x2-P8-11.png";
|
||||
png(root/"partial"/"replacements"/partial);
|
||||
place(vram,512,400,16,words(16)); q.clut_x = 512;
|
||||
pack = create(root/"partial");
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"reduced P8 min8-max11 hashes prefix4 per upstream pin");
|
||||
vram[400*1024+520] ^= 1; duck_texture_pack_invalidate(pack.get(),520,400,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"pinned prefix quirk ignores palette8 despite declared minimum8");
|
||||
vram[400*1024+512] ^= 1; duck_texture_pack_invalidate(pack.get(),512,400,1,1);
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"reduced P8 prefix change stops replacement");
|
||||
q.clut_x = 1023;
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"misaligned/edge reduced CLUT rejected");
|
||||
}
|
||||
|
||||
void st_alias_duplicates(const fs::path& root) {
|
||||
const char* ordinary = "texpage-C16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
||||
const char* st = "texpage-STC16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
||||
png(root/"paired"/"replacements"/ordinary); png(root/"paired"/"replacements"/st);
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,64,0,4,words(8)); auto q = query(vram,64,0,2,0,3,0,1);
|
||||
auto pack = create(root/"paired"); DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match_draw(pack.get(),&q,0,&match) == HD_TEXTURE_LOOKUP_FOUND && !match.key.semitransparent,
|
||||
"ordinary primitive prefers ordinary filename");
|
||||
check(duck_texture_pack_match_draw(pack.get(),&q,1,&match) == HD_TEXTURE_LOOKUP_FOUND && match.key.semitransparent,
|
||||
"semitransparent primitive prefers ST filename");
|
||||
png(root/"alias"/"replacements"/"art"/"sample.png");
|
||||
fs::create_directories(root/"alias");
|
||||
std::ofstream config(root/"alias"/"config.yaml");
|
||||
config << "MaxVRAMWriteCoalesceWidth: 1\nAliases:\n " << std::string(ordinary).substr(0,std::strlen(ordinary)-4)
|
||||
<< ": |\n art/sample.png\n"; config.close();
|
||||
pack = create(root/"alias");
|
||||
check(duck_texture_pack_match_draw(pack.get(),&q,1,&match) == HD_TEXTURE_LOOKUP_FOUND && !match.key.semitransparent,
|
||||
"bounded literal alias and opposite-convention fallback");
|
||||
DuckTexturePackInfo info{}; duck_texture_pack_get_info(pack.get(),&info);
|
||||
check(info.ignored_count && std::strstr(info.diagnostic,"MaxVRAMWriteCoalesceWidth"),"unsupported config option explicitly diagnosed");
|
||||
png(root/"duplicate"/"replacements"/"a"/ordinary); png(root/"duplicate"/"replacements"/"b"/ordinary);
|
||||
pack = create(root/"duplicate");
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_AMBIGUOUS,"recursive duplicate keys fall back deterministically");
|
||||
q.u_first = 254; q.u_last = 1;
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_NONE,"wrapped UV interval falls back");
|
||||
}
|
||||
|
||||
void failed_payloads(const fs::path& root) {
|
||||
const char* name = "texpage-C16-000CA76CB9B329DC-256x256-0-0-4x2.png";
|
||||
png(root/"budget"/"replacements"/name);
|
||||
std::vector<uint16_t> vram(1024*512); place(vram,64,0,4,words(8)); auto q = query(vram,64,0,2,0,3,0,1);
|
||||
auto pack = create(root/"budget"); DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"over-budget payload identity remains valid metadata");
|
||||
duck_texture_pack_set_decode_budget(pack.get(),8);
|
||||
check(decode_fails(pack.get(),match.entry_id),"over-budget decoded image fails before pixel allocation");
|
||||
fs::create_directories(root/"malformed"/"replacements");
|
||||
std::ofstream invalid(root/"malformed"/"replacements"/name,std::ios::binary); invalid << "not PNG"; invalid.close();
|
||||
pack = create(root/"malformed");
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"malformed PNG identity can be indexed without render-thread decode");
|
||||
check(decode_fails(pack.get(),match.entry_id),"malformed PNG fails asynchronously for native fallback");
|
||||
const fs::path oversized = root/"dimension"/"replacements"/name;
|
||||
png(oversized);
|
||||
std::ifstream file(oversized,std::ios::binary); std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(file)),{}); file.close();
|
||||
/* Forge a valid IHDR width above the decode dimension bound. Keeping the
|
||||
* tiny original IDAT makes the fixture inexpensive and verifies rejection
|
||||
* using image metadata before an enormous allocation could happen. */
|
||||
bytes[16] = 0; bytes[17] = 0; bytes[18] = 0x23; bytes[19] = 0x28; /* 9000 */
|
||||
const uint32_t crc = png_crc_update(0xffffffffu,bytes.data()+12,17)^0xffffffffu;
|
||||
for (unsigned i = 0; i < 4; ++i) bytes[29+i] = static_cast<uint8_t>(crc >> (24-8*i));
|
||||
std::ofstream changed(oversized,std::ios::binary|std::ios::trunc);
|
||||
changed.write(reinterpret_cast<const char*>(bytes.data()),bytes.size()); changed.close();
|
||||
pack = create(root/"dimension"); duck_texture_pack_match(pack.get(),&q,&match);
|
||||
check(decode_fails(pack.get(),match.entry_id),"oversized PNG IHDR fails within native fallback bounds");
|
||||
}
|
||||
|
||||
void dumping(const fs::path& root) {
|
||||
auto pack = create(root); std::vector<uint16_t> vram(1024*512);
|
||||
vram[0] = 0x3210; vram[400*1024+1] = 0x001f; vram[400*1024+2] = 0x8000; vram[400*1024+3] = 0xffff;
|
||||
auto q = query(vram,0,0,0,0,3,0,0,0,400); char error[512]{};
|
||||
check(duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"queue native ST page dump");
|
||||
check(duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)) == HD_TEXTURE_LOOKUP_NONE,"dump session deduplicates without filesystem access");
|
||||
pack.reset(); /* required to finish queued background writes */
|
||||
const fs::path file = root/"dumps"/"texpage-STP4-A92BF769517E0361-59786C612FB45363-64x256-0-0-4x1-P0-15.png";
|
||||
check(fs::is_regular_file(file),"fixed independent hash filename published at worker shutdown");
|
||||
int width = 0, height = 0, channels = 0;
|
||||
std::ifstream encoded_file(file,std::ios::binary);
|
||||
std::vector<uint8_t> encoded((std::istreambuf_iterator<char>(encoded_file)),{});
|
||||
auto* image = encoded.empty() ? nullptr : stbi_load_from_memory(encoded.data(),static_cast<int>(encoded.size()),&width,&height,&channels,4);
|
||||
constexpr uint8_t expected[] = {0,0,0,0,255,0,0,255,0,0,0,128,255,255,255,128};
|
||||
check(image && width == 4 && height == 1 && !std::memcmp(image,expected,sizeof(expected)),"dump decodes native indices/CLUT and ST alpha");
|
||||
if (image) stbi_image_free(image);
|
||||
std::ofstream owner(file,std::ios::binary|std::ios::trunc); owner << "owner edit"; owner.close();
|
||||
pack = create(root); duck_texture_pack_dump_draw(pack.get(),&q,1,error,sizeof(error)); pack.reset();
|
||||
std::ifstream edited(file,std::ios::binary); std::string payload((std::istreambuf_iterator<char>(edited)),{});
|
||||
check(payload == "owner edit","existing owner dump is never overwritten");
|
||||
pack = create(root/"upload");
|
||||
duck_texture_pack_track_upload(pack.get(),0,0,1,1,vram.data(),vram.size());
|
||||
check(duck_texture_pack_dump_draw(pack.get(),&q,0,error,sizeof(error)) == HD_TEXTURE_LOOKUP_FOUND,"tracked source emits texupload dump");
|
||||
pack.reset();
|
||||
check(fs::is_regular_file(root/"upload"/"dumps"/"texupload-P4-A92BF769517E0361-59786C612FB45363-1x1-0-0-4x1-P0-15.png"),
|
||||
"upload dump retains original source dimensions");
|
||||
/* PNG fixtures remain independent of the VRAM pointer used by replacement
|
||||
* matching, so dumping never reads already-replaced pixels. */
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const fs::path root = fs::temp_directory_path()/fs::u8path("psx-duck-texture-\xCE\xA9-"+
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
try {
|
||||
fixed_hash_vectors(); names(); page_and_decode(root/"page"); upload(root/"upload"); reload_tracking(root/"reload");
|
||||
palettes(root/"palettes"); st_alias_duplicates(root/"keys"); failed_payloads(root/"invalid"); dumping(root/"dump");
|
||||
/* Every artifact is created beneath this fresh, explicitly named test
|
||||
* root. No user-provided paths participate in recursive cleanup. */
|
||||
fs::remove_all(root);
|
||||
} catch (const std::exception& e) { check(false,e.what()); }
|
||||
if (failures) { std::fprintf(stderr,"test_duckstation_texture_pack: %d failure(s)\n",failures); return 1; }
|
||||
std::puts("PASS: DuckStation format, independent hashes, native matching, decode and dumps");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/* Real GL + real format/decoder + renderer facade. Source-owned fixtures. */
|
||||
#define PSX_TEST_HD_TEXTURE_PACK 1
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_ONLY_PNG
|
||||
#include "../third_party/stb_image.h"
|
||||
#define main scale_fixture_main
|
||||
#include "test_gl_scale_invariance.c"
|
||||
#undef main
|
||||
#include "gpu_render.c"
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "hd_texture_pack.h"
|
||||
#include "png_write.h"
|
||||
|
||||
const GpuRenderBackend* vk_backend_get(void) { return NULL; }
|
||||
void present_shot_done(int ok) { (void)ok; }
|
||||
int host_osd_needs_present(void) { return 0; }
|
||||
int psx_present_vsync_owns_cadence(void) { return 0; }
|
||||
void latency_ring_mark(LatencyStage stage) { (void)stage; }
|
||||
void psx_host_sleep_ms(unsigned ms) { SDL_Delay(ms); }
|
||||
int present_shot_take(char* out,int n) { (void)out; (void)n; return 0; }
|
||||
int host_osd_image(const uint32_t** p,int* w,int* h) { (void)p; (void)w; (void)h; return 0; }
|
||||
int host_osd_volume_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
void host_osd_present_done(void) {}
|
||||
int psx_rewind_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
float psx_rewind_slide(void) { return 0; }
|
||||
int psx_savestate_menu_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
|
||||
|
||||
static uint16_t source_words[4*4];
|
||||
static uint16_t reference[1024*512];
|
||||
static uint8_t pixels[64*64*4];
|
||||
static const uint16_t texture_page=8u|(2u<<7);
|
||||
|
||||
static void pack_png(const char* root,int st) {
|
||||
DuckTextureKey key={0};
|
||||
key.source_hash=duck_texture_hash_words_le(source_words,16);
|
||||
key.source_width_words=4; key.source_height=4;
|
||||
key.width=4; key.height=4; key.kind=DUCK_TEXTURE_UPLOAD;
|
||||
key.depth=HD_TEXTURE_DEPTH_16BPP; key.semitransparent=st;
|
||||
char stem[256],path[2048];
|
||||
check(duck_texture_format_name(&key,stem,sizeof(stem)),"fixture filename");
|
||||
snprintf(path,sizeof(path),"%s/replacements/%s.png",root,stem);
|
||||
uint8_t rgba[16*16*4];
|
||||
for(int y=0;y<16;++y) for(int x=0;x<16;++x) {
|
||||
uint8_t* at=rgba+(y*16+x)*4;
|
||||
at[0]=(x&1)?0:255; at[1]=0; at[2]=(x&1)?255:0; at[3]=255;
|
||||
if(st) {
|
||||
const uint8_t alpha[6]={0,127,128,242,243,255};
|
||||
at[3]=alpha[x%6];
|
||||
if(y>=8) at[0]=at[1]=at[2]=0;
|
||||
} else if(y>=8) {
|
||||
at[0]=at[1]=at[2]=0;
|
||||
at[3]=(x&1)?128:127;
|
||||
}
|
||||
}
|
||||
FILE* file=fopen(path,"wb");
|
||||
check(file!=NULL,"open fixture PNG");
|
||||
if(file) { check(png_write_rgba(file,rgba,16,16),"write fixture PNG"); fclose(file); }
|
||||
}
|
||||
static void state(void) {
|
||||
gr_set_draw_area(0,0,1023,511); gr_set_draw_offset(0,0);
|
||||
gr_set_mask_bits(0,0); gr_set_semi_transparency(0,0);
|
||||
gr_set_texture_window(0); gr_set_color_modulation(128,128,128,1);
|
||||
}
|
||||
static void native_scene(void) {
|
||||
state();
|
||||
gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
gr_fill_rect(0,0,64,64,0x4210);
|
||||
gr_draw_textured_rect(16,16,4,4,0,0,0,0,texture_page);
|
||||
for(int mode=0;mode<4;++mode) {
|
||||
gr_set_semi_transparency(1,mode);
|
||||
gr_draw_textured_rect(20+mode*5,20,4,4,0,0,0,0,texture_page);
|
||||
}
|
||||
gr_set_semi_transparency(0,0);
|
||||
gr_set_mask_bits(1,0); gr_draw_flat_rect(40,40,8,8,0x001f);
|
||||
gr_set_mask_bits(0,1); gr_draw_textured_rect(38,38,12,12,0,0,0,0,texture_page);
|
||||
gr_set_mask_bits(0,0);
|
||||
gr_set_texture_filter(1);
|
||||
gr_set_perspective_triangle(1,1.0f,0.5f,0.25f);
|
||||
gr_set_precise_triangle(1,(4<<16)+32768,30<<16,14<<16,30<<16,4<<16,38<<16);
|
||||
gr_draw_textured_triangle(4,30,0,0,14,30,3,0,4,38,0,3,0,0,texture_page);
|
||||
gr_set_texture_filter(0);
|
||||
gr_draw_line(3,50,53,50,0x7fff);
|
||||
gr_copy_rect(16,16,2,2,4,4);
|
||||
uint16_t masked[4]={0x7c00,0x7c00,0x7c00,0x7c00};
|
||||
gr_set_mask_bits(0,1); gr_vram_transfer_in(40,40,4,1,masked);
|
||||
gr_set_mask_bits(0,0);
|
||||
gr_fill_rect(1022,510,4,4,0x1234);
|
||||
}
|
||||
static void wait_ready(int st) {
|
||||
const int bounds[4]={0,0,3,3};
|
||||
int ready=0;
|
||||
for(int i=0;i<2000 && !ready;++i) {
|
||||
GpuHdTextureImage image={0};
|
||||
ready=gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,st,&image);
|
||||
gpu_hd_textures_release_image(&image);
|
||||
if(!ready) SDL_Delay(1);
|
||||
}
|
||||
check(ready,"async replacement decode completed");
|
||||
}
|
||||
static void capture(void) {
|
||||
uint32_t argb[64*64];
|
||||
check(gl_renderer_capture_display_hires(argb,64*4,0,0,16,16)==64*64,"high-resolution capture");
|
||||
for(int i=0;i<64*64;++i) {
|
||||
pixels[i*4]=(uint8_t)(argb[i]>>16); pixels[i*4+1]=(uint8_t)(argb[i]>>8);
|
||||
pixels[i*4+2]=(uint8_t)argb[i]; pixels[i*4+3]=(uint8_t)(argb[i]>>24);
|
||||
}
|
||||
}
|
||||
static const uint8_t* sample(int x,int y) { return pixels+(y*64+x)*4; }
|
||||
|
||||
int main(int argc,char** argv) {
|
||||
if(argc==2 && !strcmp(argv[1],"--native-baseline")) {
|
||||
char* native_args[]={"hd-baseline","4","twin","1"};
|
||||
return scale_fixture_main(4,native_args);
|
||||
}
|
||||
if(argc!=2) return 2;
|
||||
for(int i=0;i<16;++i) source_words[i]=i%4==3?0:i%4==2?0x8000:i%4==1?0x83e0:0x03e0;
|
||||
pack_png(argv[1],0); pack_png(argv[1],1);
|
||||
gr_set_backend(GR_BACKEND_SOFTWARE); gr_init(reference);
|
||||
sw_set_faithful_authority(1); native_scene(); sw_set_faithful_authority(0);
|
||||
if(SDL_Init(SDL_INIT_VIDEO)!=0) return 2;
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION,3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
SDL_Window* win=SDL_CreateWindow("HD pack regression",0,0,128,128,SDL_WINDOW_OPENGL|SDL_WINDOW_HIDDEN);
|
||||
if(!win) return 2;
|
||||
gr_set_backend(GR_BACKEND_OPENGL); gr_init(vram); gr_set_scale(4);
|
||||
char error[512]={0};
|
||||
check(gpu_hd_textures_configure(argv[1],1,1,error,sizeof(error)),error);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if(!gl_renderer_init_context(win)) return 2;
|
||||
if(s_hiw) check(hiw_ensure(0,64)!=NULL,"high-resolution window allocated before draws");
|
||||
state(); gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
wait_ready(0); wait_ready(1);
|
||||
check(gpu_hd_textures_reload(error,sizeof(error)),"live pack reload succeeds");
|
||||
/* No new upload: a reload must retain identities of resident textures. */
|
||||
wait_ready(0); wait_ready(1);
|
||||
native_scene(); gl_renderer_sync_cpu();
|
||||
check(memcmp(vram,reference,sizeof(vram))==0,"HD native VRAM matches faithful software across all operations");
|
||||
uint16_t readback[64*64];
|
||||
gr_vram_transfer_out(0,0,64,64,readback);
|
||||
int unchanged=1;
|
||||
for(int y=0;y<64;++y) for(int x=0;x<64;++x) unchanged&=readback[y*64+x]==reference[y*1024+x];
|
||||
check(unchanged,"guest GPUREAD stays native with replacements");
|
||||
|
||||
/* Source rectangle is four native texels, replacement is sixteen. The
|
||||
* first native texel contains alternating red and blue HD columns. */
|
||||
state(); gr_fill_rect(0,0,16,16,0x03e0);
|
||||
gr_draw_textured_rect(4,4,4,4,0,0,0,0,texture_page); capture();
|
||||
const uint8_t* a=sample(16,16); const uint8_t* b=sample(17,16);
|
||||
check(a[0]>200 && a[2]<20 && b[2]>200 && b[0]<20,
|
||||
"replacement detail survives inside one native texel at 4x");
|
||||
check(vram[4*1024+4]==0x03e0,"presentation red/blue never enters guest VRAM");
|
||||
check(sample(16,24)[1]>200,"non-ST alpha127 cuts out black");
|
||||
check(sample(17,24)[0]==0 && sample(17,24)[1]==0 && sample(17,24)[2]==0,
|
||||
"non-ST alpha128 retains opaque black");
|
||||
|
||||
/* ST alpha bytes encode PSX classes, including alpha0 with colored RGB.
|
||||
* Mode 1 is additive, so the green destination survives STP texels. */
|
||||
state(); gr_fill_rect(0,0,16,16,0x03e0); gr_set_semi_transparency(1,1);
|
||||
gr_draw_textured_rect(4,4,4,4,0,0,0,0,texture_page); capture();
|
||||
check(sample(16,16)[0]>200 && sample(16,16)[1]>200,"ST colored alpha0 stays semitransparent");
|
||||
check(sample(19,16)[1]>200,"ST alpha242 stays semitransparent");
|
||||
check(sample(17,16)[1]>200 && sample(18,16)[1]>200,"ST alpha127 and alpha128 stay semitransparent");
|
||||
check(sample(20,16)[0]>200 && sample(20,16)[1]<20,"ST alpha243 is opaque");
|
||||
check(sample(21,16)[2]>200 && sample(21,16)[1]<20,"ST alpha255 is opaque");
|
||||
check(sample(16,24)[1]>200,"ST black RGBA zero cuts out");
|
||||
check(sample(20,24)[1]>200,"ST opaque black follows native-zero cutout");
|
||||
check(sample(17,24)[0]==0 && sample(17,24)[1]>200 && sample(17,24)[2]==0,
|
||||
"ST black alpha127 remains occupied semitransparent black");
|
||||
gr_set_semi_transparency(0,0);
|
||||
|
||||
/* A queued draw must consume its source before a later source overwrite.
|
||||
* The next draw falls back because upload identity has been invalidated. */
|
||||
gr_draw_textured_rect(0,0,4,4,0,0,0,0,texture_page);
|
||||
uint16_t overwrite[16]; for(int i=0;i<16;++i) overwrite[i]=0x7c00;
|
||||
gr_vram_transfer_in(512,0,4,4,overwrite);
|
||||
gr_draw_textured_rect(8,0,4,4,0,0,0,0,texture_page); capture();
|
||||
check(sample(0,0)[0]>200,"queued replacement retained before source overwrite");
|
||||
check(sample(32,0)[2]>200 && sample(32,0)[0]<20,"next draw sees new native source");
|
||||
check(vram[8]==0x7c00,"new source word retained natively");
|
||||
state(); gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
gr_draw_textured_rect(513,0,3,1,0,0,0,0,texture_page);
|
||||
gr_vram_transfer_out(512,0,4,1,readback);
|
||||
check(readback[0]==0x03e0 && readback[1]==0x03e0 && readback[2]==0x03e0 && readback[3]==0x03e0,
|
||||
"self-overlap keeps sequential native texture reads");
|
||||
const int bounds[4]={0,0,3,3}; GpuHdTextureImage lease={0};
|
||||
check(!gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"self-overlap invalidates upload only after its draw");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
check(gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"upload is resident before an interrupted A0");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
gr_vram_upload_begin(512,0,4,4);
|
||||
check(!gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"A0 header invalidates identity even if payload is later aborted");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
gl_renderer_restage_vram_after_savestate();
|
||||
check(!gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"savestate restage cannot fabricate original upload identity");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
check(gpu_hd_textures_acquire_draw(texture_page,0,0,bounds,0,0,&lease),
|
||||
"fresh upload restores identity after state load");
|
||||
gpu_hd_textures_release_image(&lease);
|
||||
check(!gl_renderer_texture_banks_supported(),"texture banks explicitly refuse HD authority");
|
||||
check(gl_renderer_pass_unavailable()!=PSX_MOD_RENDER_PASS_READY,"passes refuse HD authority");
|
||||
check(gl_renderer_stereo_unavailable()!=PSX_MOD_RENDER_PASS_READY,"stereo refuses HD authority");
|
||||
GpuHdTextureDiag diag; gpu_hd_textures_get_diag(&diag);
|
||||
check(diag.applied_draws>0,"replacement diagnostics record actual GL submissions");
|
||||
/* Preserve the established Beetle numeric-key path with source origin
|
||||
* offset, native cutout and native STP authority. */
|
||||
char beetle_root[2048],beetle_png[2304],hashes[2304];
|
||||
snprintf(beetle_root,sizeof(beetle_root),"%s/beetle",argv[1]);
|
||||
snprintf(hashes,sizeof(hashes),"%s/Hashes.ini",beetle_root);
|
||||
FILE* hf=fopen(hashes,"wb"); if(hf) fclose(hf);
|
||||
snprintf(beetle_png,sizeof(beetle_png),"%s/demo-texture-replacements/%x-0.png",beetle_root,
|
||||
hd_texture_crc32_words_le(source_words,16));
|
||||
uint8_t beetle_rgba[16*16*4];
|
||||
for(int y=0;y<16;++y) for(int x=0;x<16;++x) {
|
||||
uint8_t* at=beetle_rgba+(y*16+x)*4;
|
||||
at[0]=x<4?255:0; at[1]=x>=8?255:0; at[2]=x>=4&&x<8?255:0; at[3]=255;
|
||||
}
|
||||
FILE* bf=fopen(beetle_png,"wb"); check(bf!=NULL,"Beetle PNG open");
|
||||
if(bf){check(png_write_rgba(bf,beetle_rgba,16,16),"Beetle PNG fixture");fclose(bf);}
|
||||
check(gpu_hd_textures_configure(beetle_root,1,0,error,sizeof(error)),"Beetle session opens");
|
||||
state(); gr_vram_transfer_in(512,0,4,4,source_words);
|
||||
wait_ready(0);
|
||||
gr_vram_transfer_in(516,0,4,4,source_words);
|
||||
gr_fill_rect(0,0,16,16,0x03e0);
|
||||
gr_draw_textured_rect(4,4,2,4,5,0,0,0,texture_page); capture();
|
||||
check(sample(16,16)[2]>200 && sample(16,16)[0]<20,"Beetle partial draw keeps upload-relative UV origin");
|
||||
check(vram[4*1024+4]==0x03e0 && vram[517]==0x83e0,
|
||||
"Beetle image preserves native draw RGB and source STP");
|
||||
gr_draw_textured_rect(8,4,4,4,4,0,0,0,texture_page); capture();
|
||||
check(sample(44,16)[1]>200,"Beetle native transparent zero still cuts out replacement");
|
||||
memcpy(reference,vram,sizeof(vram));
|
||||
gl_renderer_set_cpu_auth_dual(1);
|
||||
gpu_hd_textures_shutdown();
|
||||
check(!s_hd_native_authority,"session teardown clears independent authority");
|
||||
check(gl_renderer_cpu_auth_dual(),"HD teardown preserves netplay authority");
|
||||
check(memcmp(reference,vram,sizeof(vram))==0,"teardown cannot read presentation pixels into native VRAM");
|
||||
gl_renderer_set_cpu_auth_dual(0);
|
||||
check(gl_renderer_texture_banks_supported(),"session teardown restores backend capabilities");
|
||||
check(glGetError()==GL_NO_ERROR,"HD GL errors");
|
||||
printf("scale=%d checks=%d failures=%d applied=%llu dumps=%llu\n",gr_scale(),checks,failures,
|
||||
(unsigned long long)diag.applied_draws,(unsigned long long)diag.dumped_textures);
|
||||
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
|
||||
return failures?1:0;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Original source-owned GL readback-coherence regression. No retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gpu_timeline.c"
|
||||
uint64_t psx_cycle_count=0;
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
* only the authoritative surface, never the window's tiles.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
|
||||
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
|
||||
@@ -82,6 +83,8 @@ 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. */
|
||||
|
||||
@@ -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,19 @@ 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; }
|
||||
int gte_nclip_native_wide_sign(int32_t value,int* sign) { (void)value; (void)sign; return 0; }
|
||||
int gte_nclip_native_wide_previous_sign(int32_t value,int* sign) { (void)value; (void)sign; return 0; }
|
||||
void pgxp_set_projection_tracking(int enabled) { (void)enabled; }
|
||||
int pgxp_projection_tracking(void) { return 0; }
|
||||
int pgxp_load_projection(uint32_t address,uint32_t word,PGXPProjection* projection) {
|
||||
(void)address; (void)word; (void)projection; return 0;
|
||||
}
|
||||
void gl_renderer_note_wide_triangle_recovery(int enabled) { (void)enabled; }
|
||||
int gl_renderer_projective_supported(void) { return 0; }
|
||||
void gl_renderer_draw_projected_triangle(const PSXProjectedVertex vertices[3],uint16_t page,
|
||||
uint16_t cx,uint16_t cy,int raw,int semi,int perspective) {
|
||||
(void)vertices; (void)page; (void)cx; (void)cy; (void)raw; (void)semi; (void)perspective;
|
||||
}
|
||||
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 +345,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];
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "psx_sha256.h"
|
||||
#include "psx_lobby_client.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
@@ -44,6 +45,12 @@ extern "C" void psx_host_write_word(uint32_t a, uint32_t v) { psx_write_word(a,
|
||||
extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; }
|
||||
extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) { return 0; }
|
||||
extern "C" void psx_ram_reset_size_request(void) {}
|
||||
extern "C" void gpu_hd_textures_shutdown(void) {}
|
||||
extern "C" int gpu_hd_textures_configure(const char*, int, int, char*, size_t) { return 1; }
|
||||
extern "C" void gpu_hd_textures_set_dump_enabled(int) {}
|
||||
extern "C" int gpu_hd_textures_active(void) { return 1; }
|
||||
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) { *out = {}; }
|
||||
extern "C" int gpu_hd_textures_reload(char*, size_t) { return 1; }
|
||||
extern "C" void psx_projection_reset_session(void) {}
|
||||
extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {}
|
||||
extern "C" int psx_ws_x_margin(void) { return 0; }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "psx_lobby_client.h"
|
||||
|
||||
#include "gpu.h"
|
||||
#include "gpu_hd_textures.h"
|
||||
#include "cpu_state.h"
|
||||
|
||||
#include <array>
|
||||
@@ -82,6 +83,36 @@ extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) {
|
||||
return 0;
|
||||
}
|
||||
extern "C" void psx_ram_reset_size_request(void) {}
|
||||
static int hd_shutdown_calls, hd_configure_calls, hd_dump_value;
|
||||
static int hd_replacements_value, hd_reload_calls;
|
||||
static std::string hd_root;
|
||||
extern "C" void gpu_hd_textures_shutdown(void) { ++hd_shutdown_calls; hd_root.clear(); }
|
||||
extern "C" int gpu_hd_textures_configure(const char* root, int replacements, int dump,
|
||||
char*, size_t) {
|
||||
++hd_configure_calls;
|
||||
hd_root = root;
|
||||
hd_replacements_value = replacements;
|
||||
hd_dump_value = dump;
|
||||
return 1;
|
||||
}
|
||||
extern "C" void gpu_hd_textures_set_dump_enabled(int enabled) {
|
||||
hd_dump_value = enabled;
|
||||
}
|
||||
extern "C" int gpu_hd_textures_active(void) {
|
||||
return !hd_root.empty() && (hd_replacements_value || hd_dump_value);
|
||||
}
|
||||
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) {
|
||||
*out = {};
|
||||
out->root = hd_root.c_str();
|
||||
}
|
||||
extern "C" int gpu_hd_textures_reload(char*, size_t) { ++hd_reload_calls; return 1; }
|
||||
static void test_hd_activation(void) {
|
||||
char replacements[16], dump[16];
|
||||
if (!psx_mod_current_option_value("replacements", replacements, sizeof(replacements)) ||
|
||||
!psx_mod_current_option_value("dump", dump, sizeof(dump))) { ++failures; return; }
|
||||
if (!psx_mod_set_hd_texture_pack("pack", std::string(replacements) == "true",
|
||||
std::string(dump) == "true")) ++failures;
|
||||
}
|
||||
extern "C" void psx_projection_reset_session(void) {}
|
||||
extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {}
|
||||
extern "C" int psx_ws_x_margin(void) { return 0; }
|
||||
@@ -1325,6 +1356,71 @@ int main() {
|
||||
check(psx_mod_read_disc_file("S0/LEVEL.NSF", nullptr, 0, &bytes) && bytes == 69400576u,
|
||||
"files grown past 64 MiB keep their effective size");
|
||||
}
|
||||
{
|
||||
const fs::path hd_mods = root / "hd-mods";
|
||||
write_text(hd_mods / "bundled" / "runtime.hd" / "1.0.0" / "manifest.toml",
|
||||
"format_version = 5\nid = \"runtime.hd\"\nversion = \"1.0.0\"\n"
|
||||
"name = \"HD\"\nresolver = \"declarative\"\nsave_compatibility = \"shared\"\n"
|
||||
"[[target]]\ngame_id = \"SCUS-94236\"\n"
|
||||
"[[feature]]\nid = \"textures\"\nname = \"HD\"\ndefault_enabled = false\n"
|
||||
"[[resource]]\nfeature = \"textures\"\nid = \"pack\"\nlabel = \"Pack\"\n"
|
||||
"format = \"directory\"\nrequired = true\n"
|
||||
"[[option]]\nfeature = \"textures\"\nid = \"replacements\"\n"
|
||||
"label = \"Load\"\ntype = \"boolean\"\ndefault = \"true\"\n"
|
||||
"[[option]]\nfeature = \"textures\"\nid = \"dump\"\n"
|
||||
"label = \"Dump\"\ntype = \"boolean\"\ndefault = \"false\"\n"
|
||||
"[[plugin]]\nfeature = \"textures\"\nid = \"psx.hd-textures\"\n");
|
||||
write_text(hd_mods / "state.toml",
|
||||
"format_version = 2\n[[feature]]\npackage_id = \"runtime.hd\"\n"
|
||||
"id = \"textures\"\nenabled = true\n[feature.values]\ndump = \"true\"\n");
|
||||
check(psx_mod_register_activation_plugin("psx.hd-textures", test_hd_activation),
|
||||
"HD activation plugin registers");
|
||||
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error),
|
||||
"HD mods initialize");
|
||||
const fs::path expected = fs::absolute(hd_mods / "texture-packs" / "SCUS-94236");
|
||||
check(fs::is_directory(expected / "dumps") && fs::is_directory(expected / "replacements"),
|
||||
"default HD directories exist before first launch, even without dumping");
|
||||
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
|
||||
"default pack resource commits without manual folder selection");
|
||||
const int shutdown_before = hd_shutdown_calls;
|
||||
mod_runtime_activate_plugins();
|
||||
check(hd_shutdown_calls == shutdown_before + 1 && hd_configure_calls == 1 &&
|
||||
hd_root == expected.string() && hd_replacements_value == 1 && hd_dump_value == 1,
|
||||
"HD activation clears previous session and reads owning committed options");
|
||||
char value[16] = "old";
|
||||
check(!psx_mod_current_option_value("dump", value, sizeof(value)) && value[0] == '\0',
|
||||
"owning option context does not escape the trusted callback");
|
||||
check(!psx_mod_set_hd_texture_pack("pack", 1, 1),
|
||||
"pack configuration requires the trusted resource callback context");
|
||||
hd_replacements_value = 0;
|
||||
check(psx_mod_set_hd_texture_dump(0) && hd_dump_value == 0,
|
||||
"live dump control forwards the requested switch");
|
||||
check(!gpu_hd_textures_active() && psx_mod_set_hd_texture_dump(1) && hd_dump_value == 1,
|
||||
"dump-only session can turn capture back on after turning it off");
|
||||
check(psx_mod_reload_hd_texture_pack() && hd_reload_calls == 1,
|
||||
"pack reload forwards to renderer");
|
||||
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), "HD plan clears");
|
||||
mod_runtime_activate_plugins();
|
||||
check(hd_shutdown_calls == shutdown_before + 2 && hd_configure_calls == 1,
|
||||
"disabled HD feature leaves no previous session pack active");
|
||||
check(!psx_mod_set_hd_texture_dump(1), "live dump control refuses an unconfigured session");
|
||||
const fs::path custom = hd_mods / fs::u8path(u8"chosen pack # % \u65e5\u672c");
|
||||
fs::create_directories(custom);
|
||||
write_text(hd_mods / "state.toml",
|
||||
"format_version = 2\n[[feature]]\npackage_id = \"runtime.hd\"\n"
|
||||
"id = \"textures\"\nenabled = true\n[feature.resources]\npack = \"" +
|
||||
custom.generic_u8string() + "\"\n");
|
||||
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error) &&
|
||||
PSXRecompV4::mod_runtime_commit(stock_path, &error),
|
||||
"chosen Unicode pack path resolves and persists through commit");
|
||||
mod_runtime_activate_plugins();
|
||||
check(hd_root == custom.generic_u8string(), "renderer receives the chosen folder as UTF-8");
|
||||
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error) &&
|
||||
PSXRecompV4::mod_runtime_commit(stock_path, &error),
|
||||
"committed Unicode pack path reloads without corruption");
|
||||
mod_runtime_activate_plugins();
|
||||
check(hd_root == custom.generic_u8string(), "Unicode pack root survives the complete session cycle");
|
||||
}
|
||||
fs::remove_all(root, ec);
|
||||
if (failures) return 1;
|
||||
std::cout << "mod runtime tests passed\n";
|
||||
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
xxHash Library
|
||||
Copyright (c) 2012-2021 Yann Collet
|
||||
All rights reserved.
|
||||
|
||||
BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this
|
||||
list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Vendored
+7238
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user