Skip to content

[GPU] Integrate performance audit and Silent Hill native occlusion improvements - #23

Draft
xnetcat wants to merge 87 commits into
mainfrom
perf/cold-cache-libs-fixes
Draft

xnetcat wants to merge 87 commits into
mainfrom
perf/cold-cache-libs-fixes

Conversation

@xnetcat

@xnetcat xnetcat commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Before submitting

Contribution guidelines.

This is the combined performance-audit PR: the complete round-one integration through cde04b713, both committed round-two fixes, and the accepted recovered K1–K21 implementation and regression tests. It fixes avoidable guest/runtime overhead, cache retention costs, Metal ownership/order defects and split-transport coordination. The six baseline Libs failures are repaired with their behavioral assertions retained.

Current default policy: eleven enabled settings

Small query/barrier drains can make the drawing thread wait for a resolver wakeup. The new bounded caller-resolution path claims an untouched FIFO head only when at most eight jobs remain, with no active/claimed worker, no pause, and an open dependency gate. It retains speculative reads, input/ownership validation, query ordering and exception policy. SHARPEMU_METAL_V2_CALLER_RESOLVE=0 retains worker-only resolution. All prior commits and ten accepted defaults remain; no resolution, VSync, quality or GeForce NOW setting changed.

Acceptance follows the requested component-benchmark policy; no Silent Hill FPS gain is claimed:

Measurement Disabled → enabled
ARM64 single-job wall / whole-process CPU 2,874 → 542 ns / 4,787 → 962 ns
x64 single-job wall / whole-process CPU 3,160 → 972 ns / 5,510 → 1,711 ns
ARM64 empty-program burst 32, documented cost 329 → 356 ns wall; 655 → 691 ns process CPU
Same-payload stationary Silent Hill room, 375–410 seconds 16.48 → 16.49 FPS median

Seven alternating paired component rounds, native/Rosetta .NET 10.0.3 normal tiering, matching caller/worker QoS; allocation about 272 B/job in both modes. Larger batches are mostly flat. Runtime ranges were 14.00–17.93 disabled and 11.98–18.98 enabled under different paging pressure. Resolver wait fell while idle time rose. 30 FPS remains unmet.

The full Metal run also exposed an existing fixture ownership leak. A temporary isolated registration-balance diagnostic failed with caller resolution disabled (zero registrations before, one after the volume fixture), then passed after cleanup. Volume/sRGB fixtures now retain and dispose their owning images; tiler lifetime assertions remain intact. The original failed run is documented, and the temporary diagnostic is archived rather than shipped. Production texture ownership is unchanged.

Final validation: Libs x64, all feature overrides unset: 9,100 passed / 0 failed / 5 existing skips; Metal ARM64, required device, feature overrides unset: 1,639 passed / 0 failed / 7 existing skips; Metal ARM64, explicit caller-resolution fallback: 21 passed / 0 failed / 0 existing skips. Release runtime publishes, REUSE and whitespace checks passed. Earlier enabled/disabled/command-thread checks and the complete benchmark matrix are retained in the report.

Caller-resolution measurements and failure triage · All eleven defaults and fallback switches. PR remains draft and unmerged; the required human-authorship attestation remains unchecked.

The sections below retain the source/configuration scope of earlier validation rounds.

v23 runtime verification and next bottleneck

A clean 4b6bad96b38cabfc82c936efd0f561572b02deb9 x64 CLI/native ARM64 GPU-host build passed both publishes and all 471 payload hashes. With all ten promoted switches unset, the verified stationary Silent Hill room measured 1.0 FPS median (0.0–4.49, 18 reports) in the preselected 375–410-second window. This is a poor result under heavy system paging, not an isolated layout-history comparison or evidence of 30 FPS. Seven system samples recorded 304,736 swap-in / 310,316 swap-out pages. Resolution, scale, VSync, quality and GeForce NOW settings were unchanged.

The layout snapshots recorded 12,645 history hits among 196,608 queries (6.43%), with 512 entries / 190,560 accounted payload bytes at the final snapshot. Eleven PERF intervals ending inside the FPS window recorded 31,565 occlusion-dump drains / 37,740 total drains (83.6%). Prefetch wait was 28,478 ms versus 2,442.5 ms resolver wall time; these overlapping counters are not exclusive CPU costs. The next investigation is query-related draining and resolver handoff, while preserving query snapshots and dependent reads. No query-deferral default was added.

The run stopped through its own marker; both processes ended. The guest CPU trap occurred after the stop marker and remains an unresolved teardown issue. Full runtime evidence and limitations includes memory, CPU and GC windows. This follow-up changes documentation only; the test and benchmark counts below remain those of the tested code commits.

Default performance follow-up: ten enabled settings

The nine accepted defaults remain enabled, and bounded vertex-layout history now defaults on (SHARPEMU_VERTEX_LAYOUT_HISTORY=0 retains the last-layout-only path). Acceptance follows the maintainer's request to include validated component wins without requiring an isolated Silent Hill FPS gain. All previous commits are preserved; this adds c5ae56c56 (CPU-clock correction) and 4b6bad96b (layout history and cold metadata allocation fix).

A shader alternating among meshes can reuse immutable layout arrays after fresh guest reads, exact table-byte checks and metadata identity checks. The cache has 512 slots and a 2 MiB additional payload budget, plus its slot array. Low reuse triggers bounded backoff; it periodically retries when a working set changes. Each draw still gets its own mutable input wrapper. Cold table parsing avoids allocating a metadata object that was immediately discarded.

Final component measurement, default tiering History off → on
ARM64, two layouts, median calling-thread CPU 214.25 → 122.31 ns/resolution
x64, two layouts, median calling-thread CPU 193.78 → 109.91 ns/resolution
Cycles of 2–64 layouts, both architectures 37–43% lower CPU medians, 488 → 216–233 B/resolution
1,024-layout / always-changing CPU medians +0.1% to +5.3%, no win claimed; elapsed times show larger host contention
Cold shader identity allocation, preserved pre-change library → final 600 → 584 B/resolution; no isolated timing-win claim

Nine alternating 200,000-operation rounds after 10s/5s warmups. Native/Rosetta timings and allocation remain distinct from game FPS. The full table and methodology include elapsed medians, miss costs, old-library controls and limitations. The current defaults matrix documents all ten switches and conditional command-thread features.

The benchmark helper now reads Mach THREAD_BASIC_INFO: a C control showed CLOCK_THREAD_CPUTIME_ID returning unscaled ticks under Rosetta on this host. Earlier published dense-register CPU comparisons were native ARM64 and remain valid. Old Rosetta samples from that clock must not be interpreted as nanoseconds.

Final validation: Libs x64 9,084 passed / 0 failed / 5 existing Opus skips; focused resolver/pipeline ARM64 109/0/0 (overlapping configuration checks). The Release benchmark-tool build and both architecture CPU timing smoke runs passed. REUSE 2,874/2,874 and whitespace checks passed. No game was launched or new app bundle published in this follow-up. GeForce NOW settings were unchanged. 30 FPS remains unachieved; the runtime objective stays open. The PR remains draft and unmerged; the human-authorship attestation remains unchecked.

Default audit and benchmark acceptance follow-up

All nine validated performance settings remain enabled by default, including accepted component-benchmark wins without an isolated Silent Hill FPS gain. All earlier commits are preserved. The default matrix documents the fallback switches and conditional command-thread features.

A new dense-register range-copy experiment was rejected and removed from production. Its first variant improved CPU-time measurements with tiering disabled but regressed four of five normal-tiering cases by roughly 10–11%. A fixed-size variant improved several cases but left indexed draws 13.5% slower, with its two controls only about 3% apart. Complete benchmark tables, methods and limitations. The rebuilt production library matches the prior baseline byte-for-byte; isolated copy wins did not justify a new default.

Retained in 58996827f: optional macOS calling-thread CPU timing alongside wall timing in the synthetic tool; two interpreter regression cases; and a polling-fixture CI race fix. The stopwatch now starts before the writer, and the writer is joined before shared memory is freed even after an assertion failure. Production polling and timeout bounds are unchanged.

Final restored-source Release validation: Libs 9,070 passed / 0 failed / 5 existing skips, GPU-remote 56/0/0, combined 9,126/0/5. REUSE 2,869/2,869 and whitespace checks passed. No game was launched in this follow-up; 30 FPS remains unachieved. Earlier runtime and suite results below retain their original source/configuration scope.

Local draw allocation follow-up (opt-in)

The nine validated performance defaults remain enabled, including component-benchmark wins without an isolated Silent Hill FPS gain. Bundle and launcher inspection found no overrides disabling them.

This follow-up adds a bounded, local-only cache of 256 completed deferred draw wrappers. References and arguments are cleared after execution and state restoration, including failures; producer/sink work, register snapshots, geometry snapshots and public resolver jobs retain their existing ownership. SHARPEMU_LOCAL_DRAW_REUSE=1 opts in; unset or 0 keeps the allocating path.

Real translation queue/resolver component, ARM64 and x64, tiering disabled Reference Reuse
Bursts 1, 5 and 32: render-thread allocation 536 B/draw 352 B/draw
Burst 512: render-thread allocation 536.169 B/draw 444.169 B/draw

Nine alternating rounds per case. This removes 184 B/draw within the bound, or 92 B/draw when half a burst exceeds it. Timing is mixed: tiering-disabled medians differ by up to approximately 2.1%, while normal-tiering cases include roughly 7% and 15% regressions as well as gains. No consistent component timing win is claimed.

Three same-binary, same-seed Silent Hill controls at 1080p / scale 1 / VSync on measured 5.97 / 7.98 / 6.485 FPS (enabled / disabled / enabled), with ranges 3.50–7.49 / 6.98–8.98 / 3.49–9.99 and 17 / 18 / 18 reports in preselected 250–285-second windows. System swap-out counts were 24,556 / 7,056 / 110,664 pages. The return fell from near 9–10 FPS to 3.49 FPS. These sequential captures do not isolate the code's effect, and the return does not exonerate it. The new option therefore remains opt-in pending better timing evidence. GeForce NOW settings were not altered. 30 FPS remains unachieved.

The candidate passed 9,068/0/5 Libs x64, 1,637/0/7 required-device Metal x64, plus 8/0/0 each for focused ARM64 normal-tiering and ARM64/x64 tiering-disabled controls. Both runtime publishes passed; 471 payload hashes were verified before each game. The final opt-in source was rebuilt and rerun with all SHARPEMU overrides removed: 9,068/0/5 Libs x64 passed, 232.769 seconds including launcher/build time. REUSE passed 2,868/2,868 files and whitespace checks passed. No shader suite rerun is claimed for this follow-up. Raw runtime/trace data remains local; runnable component benchmark source is included.

Exact timings, ownership tests, source hashes, runtime windows, GC measurements and limitations.

Enabled by default in fb71707

At the maintainer's request, positive component evidence is sufficient for default promotion even without an isolated Silent Hill FPS gain. This follow-up enables nine paths: native occlusion, packed-format loads, static scratch indexing, scalar constant buffers, graphics binding reuse, unchanged-frame presentation reuse, audio queue leaf dispatch, asynchronous command-item reuse, and eligible queued CP writes. Each retains an explicit =0 fallback. Queued CP writes and recycling apply when command threading is requested; command overlap itself remains opt-in. Background program resolution is separate and was already enabled by default (SHARPEMU_METAL_V2_PARALLEL_RESOLVE=0 disables it).

Default settings, measured evidence, retained opt-in decisions and disable switches.

Fresh default-configuration Release suites: 14,276 passed, 0 failed, 12 existing skips (ShaderCompiler.Metal 3,589/0/0; Gpu.Metal 1,635/0/7; Libs 9,052/0/5). 255 additional focused configuration checks passed: 56 with command overlap, then 56 Metal + 114 shader + 29 Libs with all nine switches disabled. REUSE 2,864/2,864; whitespace checks pass. Clean-source fb717072a self-contained x64 CLI and native ARM64 GPU-host publishes also pass; all 471 runtime payload hashes were verified. Its bounded 900-second cold-start diagnostic reached the opening cutscene, but not the stationary comparison room; no new room-FPS comparison is claimed. It exited 0 at the harness deadline. CI passed for fb717072a. A stale opt-in expectation failed on the first shader attempt; its explicit-disabled reference and a new default-behavior test passed in the final full rerun. No pixel or numerical oracle was weakened.

Scalar-packet reuse, geometry capacity and LDS forwarding retain their opt-in status because relevant workloads regressed. Other unproven shader/overlap experiments remain opt-in. Resolution, VSync and guest quality are unchanged. This promotion is not a new game-FPS measurement; 30 FPS is still unverified.

Runtime controls and synthetic fixture follow-up

The nine validated performance defaults remain enabled, including paths supported by component benchmarks without an isolated Silent Hill gain. 30 FPS remains unachieved. Clean b4cc8292e runtime publishes passed and all 471 payload hashes were verified. The same stationary room, 1920×1080 / scale 1 / VSync on, warmed seed, and preselected 250–285-second window produced:

Sequential control Median FPS Range Reports
v21, original diagnostic GC environment 8.97 7.48–10.99 17
v21, existing application 64 MiB GC budget 7.97 5.98–9.50 17
Return to unchanged v20 eabcd9f65 8.47 7.47–9.50 17

The lower results are not unique to the new code and do not isolate its effect. They do not reproduce the earlier 15.48 FPS result. System-wide paging/pressure and sequential host conditions remain comparison limits. GeForce NOW and its settings were unchanged. All three bounded runs stopped with process/wrapper exit 0; post-stop CPU traps remain recorded as teardown behavior.

The private diagnostic launcher omitted a GC setting already present in the bundle and standard launcher. The parity control verified inheritance in both processes. Separate event-only captures after FPS measurement showed 137 → 20 GC pauses and 477 → 233 ms total pause time, with longer individual pauses and roughly unchanged 1.15–1.17 GB sampled allocation volume per 10 seconds. This is not a new production optimization or an FPS gain. Exact windows, allocation/CPU measurements, provenance and limits.

One of two macOS CI executions for b4cc8292e failed two synthetic 64/65-view protection-boundary cases; the other passed. The fixture's fixed synthetic guest range could overlap its pinned host allocation under ASLR. It now places the guest range beyond that backing and logs the addresses; production protection logic and all boundary assertions are unchanged. The original CI log lacks addresses, so that precise collision cannot be proven retrospectively. Fresh serial Release Host.Memory validation: 486 passed, 0 failed, 6 existing skips each on ARM64 and x64. REUSE 2,865/2,865 and whitespace checks passed. New-head CI remains separate from these local results.

Completion-read follow-up in b4cc829

The host now avoids another Metal completion-event read when its known completion has reached the highest serial submitted to the queue. The upper bound is published before native commit; recorded but unsubmitted work cannot advance the event, and pending submissions still query the live value. The existing serial-hint disable switch retains the reference path. A device regression covers three successively gated submissions and checks completed serials and GPU-written words.

Same-host getter microbenchmark, 9 alternating rounds × 1,000,000 reads Native query Guarded getter
ARM64, all submitted work completed 3.020 ns 1.319 ns
ARM64, submitted work pending 3.241 ns 2.981 ns
Rosetta x64, all submitted work completed 10.002 ns 2.297 ns
Rosetta x64, submitted work pending 5.864 ns 6.209 ns

Both arms allocate zero bytes and produce equal serial checksums. The x64 pending case costs 0.346 ns/read more; no game-FPS gain or runtime shortcut-hit ratio is claimed. Fresh Release compilation, 52/0/0 focused ARM64, 1,637/0/7 full x64 Metal, and 52/0/0 each in disabled-hint/polling-completion controls passed. REUSE 2,865/2,865 and whitespace checks pass. Shader/compiler/Libs production code is unchanged in this follow-up. Implementation, benchmark command and limits.

A same-v20-payload control using the same warmed seed and only SHARPEMU_METAL_V2_PARALLEL_RESOLVE=0 measured 10.99 FPS (6.49–14.47, 17 reports) in the same stationary room, versus 15.48 with resolution enabled. Host conditions still limit causal precision; this provides no reason to disable the existing background-resolution default. Post-window managed stack traces are biased toward runtime/native transitions and are not exclusive CPU or contention measurements. 30 FPS remains unachieved.

Register-copy follow-up in eabcd9f

Indexed triangle-list mesh draws now reuse the read-only user-config snapshot; indexed strips retain a private topology-conversion copy. Ordered-append copies allocate only the cloned counter array, removing the empty array that the previous constructor immediately replaced. Snapshot/counter independence and topology behavior are covered.

Counter-copy benchmark, nine alternating-order rounds, tiering disabled Before After
ARM64 median 33.331 ns 28.991 ns
Rosetta x64 median 58.851 ns 51.310 ns
Allocated bytes/copy 208 120

Fresh serial Release Libs: 9,060 passed, 0 failed, 5 existing skips, plus two passing architecture-specific benchmark executions. REUSE 2,864/2,864 and whitespace checks pass. These component results do not establish an FPS gain. The copy changes were subsequently exercised in the same-payload v20 room controls below; no isolated copy-fix FPS gain is claimed.

Benchmark source and updated runtime report include commands and qualifications. The v19 default-settings run still showed cold pipeline-compilation stalls. Its peak main-process footprint was 9,460 MiB. A synthetic stack probe showed that native sampling greatly inflated Rosetta VM-object stack counts while process footprint barely changed; stack size remains unchanged. 30 FPS remains unachieved.

Default-settings room controls, runtime eabcd9f

Both Release runtime architectures published successfully, with all 471 payload hashes verified. All nine promoted switches were unset, at 1920×1080 / scale 1 / VSync on. The stationary shelf/lamp room was visually verified in both runs.

Same runtime payload, sequential runs Window Reports Median FPS Range
Detailed render/pass profiling on 375–410 s 17 11.95 7.99–14.96
Detailed render/pass profiling off 205–240 s 17 15.48 13.96–16.97

Cache warmth and changing host load prevent attributing the full difference to profiling. These runs also do not isolate the defaults or copy fix against an earlier build. 30 FPS remains unachieved. GeForce NOW and rendering quality were unchanged.

The profiled run's median summed encoder duration was 19.251 ms/frame (overlapping durations, not exclusive GPU frame time). Leading GPU-worker phases were program-prefetch wait (12.311 ms/frame), register writes (7.338), command interpretation (7.332), and resource preparation (7.273). Separate ten-second thread captures recorded worker CPU usage of 8.943 s with profiling and 6.533 s without; the unprofiled guest RHI thread used 6.051 CPU-s. CPU draw processing, resolver handoffs and guest command production remain investigation targets.

Both runs stopped through their own markers and exited process/wrapper 0. The profiler-off run's CPU-trap lines occurred after its stop marker; this is not proof of clean guest teardown. Full source, timing, memory and cleanup qualifications. This follow-up changes documentation only: REUSE 2,864/2,864 and whitespace checks pass; production suites were not repeated. CI passed on runtime source eabcd9f65; the new documentation head has its own pending checks.

Silent Hill first-room follow-up

This update includes all accumulated source changes used in the approximately 14-FPS room builds, their regression tests, synthetic benchmarks, and the later diagnostic/correctness follow-ups. It preserves the existing commit history. The principal paired improvement is native Metal occlusion: correct visibility counts let guest predication avoid draws that the previous conservative always-visible answer retained.

Same binary, same stationary room, 1080p / scale 1 / VSync on Native occlusion off Native occlusion on
Median guest FPS, 18 reports per arm 4.485 13.43
Report range 3.50–5.00 10.45–14.44

This is a 2.994× observed median ratio in the first room, not full-game performance or the user's original 2-FPS scene. A later combined v13 run observed 13.955 FPS (11.99–14.95); it is not an isolated audio-path gain. 30 FPS remains unachieved. The latest profiled v18 room result was 6.98 FPS under compression/swapping pressure; the report retains that result and excluded/mismatched runs.

Additional changes

  • Native visibility intervals, ordered GPU counter snapshots, ready-bit publication and bounded 16-MiB query storage; sparse/reset/predication and queue-order tests.
  • Pending-label overlap invalidation, queued CP/qword-write ordering, command producer lifecycle and commit coalescing corrections.
  • APR admission/read leases and shutdown ordering before archive unmount; managed leaf-import shutdown safe points.
  • Exact resident materialization comparison, dense shader-register lookup, one-allocation mip layout construction, color/depth target memoization and the depth-address collision fix.
  • Negative page filters for held labels, one-pass sampler numeric classes, graphics-binding and unchanged-frame presentation reuse.
  • Audio query leaf dispatch and bounded asynchronous command-item recycling; scalar-packet reuse remains opt-in.
  • Conservative shader analysis corrections; packed-format, scalar-constant and scratch-slot paths default on, while mesh LDS, output-capacity and unobserved-write experiments remain opt-in.
  • Scripted-only diagnostic input, mapping/resource-origin diagnostics, render phase and memory telemetry, and reproducible synthetic benchmark cases.

The historical measurements below retain their original explicit flags. The follow-up default policy above supersedes the earlier opt-in decisions for the nine promoted paths; it does not change any recorded measurement.

Additional synthetic measurements

Workload Before After Qualification
Held-label lookup, 4,096 copies, interpage gap, native ARM64 1,944.69 ns 3.78 ns Negative filter; exact overlap fallback retained; first-hit cases cost ~2 ns more
Four alternating depth targets, native ARM64 174.88 ns / 136 B 51.93 ns / 0 B Same payload, only Libs assembly changed
Four alternating depth targets, Rosetta x64 351.10 ns / 136 B 63.78 ns / 0 B Repeated single-target case regressed 3.77 ns (~8%)
Async command items, native two-thread queue 389.965 ns / 80.0008 B 354.5175 ns / 0.026 B Default enabled; no accepted runtime FPS gain
Audio queue query dispatch ~369 ns / 40 B ~84.5 ns / 0 B Default enabled; guest audio thread remains busy

These component measurements cannot be summed or converted into FPS. Full runtime/benchmark report records exact flags, source snapshots, windows, host conditions, regressions, skips and unresolved failures. Game-derived captures and binaries remain local.

Pre-promotion four-suite validation at 35eb5deee: 16,466 passed, 0 failed, 13 existing skips — Libs 9,052/0/5; Gpu.Metal 1,634/0/7; ShaderCompiler 2,192/0/1; ShaderCompiler.Metal 3,588/0/0. Serial Release checks, required device on Metal only. Both runtime architectures publish successfully; REUSE 2,863/2,863 and staged whitespace checks pass. One trailing template blank line was removed after the suite build. This is not a fresh rerun of every solution project. Earlier native-worker timeouts, mapping failures, unmatched runtime controls and cold-cache timing limitations remain documented.

Original audit changes included

Area Implemented behavior
CPU/HLE AGC builder leaf crossings, native clocks and host-thread identity, shared thread state, in-place memset, expected-result logging and disabled-formatting guards
Scheduling Direct wake/shared resume stub, bounded owner-aware mutex spinning, per-fiber gates, targeted ULT wakes, TLS cache, sleep/yield and thread/exit corrections
Draw preparation Shader/pipeline lookup reuse with fresh-memory fallback, register snapshot reuse, scratch index expansion, binding/residency reuse and diagnostic-only write history
Materialization Content-key reuse across moving descriptors, cheaper pointer keys, linear read merging and bounded retention after actual evictions plus sustained misses; full fresh walks, mapping/bounds checks and immutable snapshots remain
Metal memory Page-granular staged refresh, reusable retile storage, GPU-pristine snapshots, owned texture-view caching, alias transitions and narrow consumer transfers
Synchronization Exact GPU-word fences, completion callbacks remain pending through publication, store/barrier tracking, waiter registration and bounded lost-wake tests
Split transport Unified protocol 7, frame-boundary waits outside the submission gate, event-driven input, one-use display reservations and pending-writeback page summaries
Shaders Relaxed-math/early-depth handling, global-record reuse, grouped raw loads, native min/max and reciprocal work; dynamic dword-format fetches with current-descriptor/bounds guards and a diagnostic disable switch; precise sqrt retained
Audio/host AJM formatting/environment overhead, audio context work, helper/compiler thread policy and archive/prewarm handling
Validation fixes Six Libs fixture/API corrections; aligned scattered views; meaningful lossy allocation footprint; enumerated live malloc ranges; correctly ordered fake-store writes

Full implementation coverage and exclusions · K1–K21 dispositions and full evidence.
Rejected experiments are documented, not silently enabled: concurrent tiling and contraction remain opt-in; unsupported lock-free/parallel planning, additional store/barrier elision, decoder pooling and ABI changes are excluded. Temporary ELF surveys, raw logs, commercial captures, local diagnostics and eboot binaries are outside the PR.

Benchmarks included

The PR now includes the previously local 26-source runtime benchmark harness, in addition to the dedicated materialization harness and GPU/input benchmarks. The tools generate their own streams, shader instructions and data; they require no game assets.

Measured results

These experiments have different baselines. The first three rows use final combined-build admission-disabled/enabled ABBA controls; the other rows are historical isolated-component comparisons. They cannot be added together or converted into FPS gains.

Workload Before After Qualification
Cold allocation, tiering disabled 8,264 B/op 6,379.9–6,382.6 B/op ~23% reduction; general latency win remains unproven
Cold allocation, tiered/PGO 8,160 B/op 6,378.9–6,384.2 B/op ~22% reduction; substantial host-control drift
Stale allocation, tiering disabled 8,392.2–8,392.3 B/op 7,032.6–7,035.4 B/op ~16% reduction
Strided translator, x64 ABBA 29.9 / 25.9 ns; 47 B/op 4.9 / 5.3 ns; 0 B/op Isolated K17; single-view case slightly slower and ~40 KiB inline storage
Fragmented translator, x64 ABBA 153.1 / 155.8 ns; 45.2 B/op 20.8 / 22.2 ns; 0 B/op Isolated K17
Input delivery p50 3.48 / 3.11 ms 0.10 / 0.10 ms Same-process transport halves; not guest draw latency
Narrow linear/tiled consumer retile 16,588,800 / 16,711,680 bytes 15,360 / 983,040 bytes Deferred byte work; remaining work still occurs on full consumption
Grouped-load / min-max / reciprocal GPU kernels 9.173 / 13.700 / 8.628 ms 4.972 / 8.215 / 6.303 ms Best of three warmed synthetic runs; precise sqrt in both arms

Full tables retain both medians, p95 ranges, controls, allocations and reproduction commands. Cold timing against historical main/cde and two admission-disabled parallel test-host stalls remain unresolved. Compiler QoS/hints have no established general speedup; concurrent tiling regressed representative writebacks and stays opt-in.

Shadow mesh / portable vertex-fetch follow-up

A bounded diagnostic reproduced 2.49–2.50 guest FPS. Five layered shadow encoders averaged
281 ms combined, with 99.7% of measured time in vertex/tiling. Three mesh shaders each contain
32–35 dynamic formatted-fetch sites. Their generated MSL was matched to the recorded cache.

A generic fast path now handles eligible identity-swizzled 32-bit fetches using the current
format and bounds, including float3-to-XYZW defaults. Other shapes retain the decoder;
SHARPEMU_MSL_RUNTIME_DWORD_FORMAT=0 restores it for comparison. No title-specific branch.
The offline compile tool now applies runtime portable-buffer switches, correcting its prior
miscompilation of normalized descriptors. No commercial shader source/data is included.

Synthetic GPU results on Apple M4/macOS 27.2, median of four alternating-arm round minima:

Workload Before ms After ms Change
32x4-float-identity 2.0751 0.1396 -93.27%
32x4-uint-identity 1.7670 0.1425 -91.94%
32x3-float-missing-w 1.8795 0.1423 -92.43%
32x1-float-missing-yzw 1.4884 1.5353 +3.15%
16x4-float-fallback 2.1888 2.2610 +3.30%
32x4-float-swizzle-fallback 2.0894 2.1261 +1.76%

Supported fetches improve 12.4–14.9× in this cache-resident compute microbenchmark;
fallback cases cost 1.8–3.3% more. Broader versions with 26–27% fallback regressions were
rejected. These synthetic numbers are not shadow-pass or FPS speedups. The separate
runtime comparison below measures the title scene.

Validation: required-device Release Metal shader suite 3,302 passed / 0 failed / 0 reported
skips
; focused correctness + opt-in benchmark 6 passed, including 1,600 changing-descriptor/
bounds cases; 896 offline shader records translated without failures or unstable outputs;
all three regenerated shadow mesh libraries compiled with Metal; REUSE passed.
Full reproduction details, source mapping, caveats and timings:
portable vertex-fetch report.

Verified Silent Hill title-scene comparison

Fresh Release x64 emulator + native ARM64 GPU host from clean production source
3cf2d655e485aea8faf55ce767b6bdfba192756f, Apple M4, .NET 10.0.3. Both arms use the
same executable and copies of one user/cache state, 1920×1080, scale 1, VSync on,
HDR auto and strict compute. Only SHARPEMU_MSL_RUNTIME_DWORD_FORMAT=0/1 differs.
Both final arms disable shader prewarming to remove background-compilation work.
Only Silent Hill was launched; other apps remained open.

30-second observed interval Fast path disabled Fast path enabled
Median guest FPS (15 reports) 2.49 3.98
Guest FPS report range 1.99–2.50 3.00–4.00
Mean target shadow encoder 56.509 ms 26.753 ms
Shadow encoder observations 350 565
Reported library/pipeline compilation time 0 ms 0 ms

The five shadow encoders retain matching shader identities, draw counts and logical
mesh threadgroups. Mean encoder duration falls 52.7%; observed median title-scene
FPS rises about 60%. Both runs exited 0 after controlled shutdown. Screenshots
confirm the rooftop/title menu, not a traversal of playable levels. One malformed
baseline pass-log line was excluded and recorded. A normal-prewarming pair corroborates
the result but retains background library work in its control and is not the primary
comparison. Host memory pressure remained present; this is a paired observation, not
a confidence interval or a universal FPS promise.

Remaining cost: a 72×41×96 layered 46-draw pass averages 41.263 → 42.340 ms, with
no demonstrated improvement; the five shadows still average roughly 27 ms each.
Four FPS is still poor. Earlier cold-start, disk-full and free-space-guard captures
are excluded. The first two also exposed an APR/ZArchive shutdown disposal race;
that race was subsequently repaired with APR admission/read leases and teardown ordering in the first-room follow-up above. Full methodology, per-pass measurements,
source/run identifiers and exclusions: runtime report.

Testing

  • Runtime report follow-up 52f07c7b3: tracked edits are documentation only; REUSE passes 2,819/2,819 files and whitespace checks pass. Both final Silent Hill runs exit 0, and all 470 published runtime file hashes remain unchanged after testing. Prior suite counts below retain their original source attribution.

The original integration was synthetic-only. Subsequent authorized Silent Hill runtime
tests localized and measured the shadow bottleneck. The earlier result applies to the rooftop/title scene; the new native-occlusion pair applies to the stationary first room. Neither establishes full-game performance.

  • Original combined implementation 18323b718: 16,741 passed, 0 failed, 13 existing skips, one final run per project. Release solution, pinned x64 smoke build, REUSE and both macOS CI builds passed.
  • Libs 8,943/0/5; ShaderCompiler 2,167/0/1; required-device ShaderCompiler.Metal 3,296/0/0; required-device Gpu.Metal 1,573/0/7; leak suite 19/0/0 (passed/failed/skipped).
  • Remote/Split 56/0/0 and 34/0/0, with x64-to-arm64 smoke and five additional passing repeats of the corrected blocking fixture. Native Host.Memory/Core.Memory 517/0/0 and 7/0/0.
  • Admission-disabled serial suite 2,167/0/1. Two parallel stalls and an earlier nonreproduced replica-worker abort are preserved as unresolved evidence, not claimed fixed.
  • Benchmark packaging follow-up: standalone Release build passes; 75 short benchmark cases complete; generated Metal triangle asserts exact pixel 0xFF0000FF; REUSE passes all 2,816 files; whitespace checks pass. Quick-run timings are functional smoke evidence, not new performance claims. This follow-up changes tooling/docs and the tool's Metal friend-assembly declaration, not emulator algorithms. CI reruns on the new head.

This PR was prepared by an AI implementation agent at the maintainer's explicit request. The human-authorship attestation remains unchecked rather than falsely asserted.

Checklist

  • I have read and followed CONTRIBUTING.md, with the explicitly requested combined scope.
  • I tested my changes or marked the testing section as N/A.
  • I wrote this pull request description myself and did not paste AI-generated explanations.
  • I listed the game(s) I tested (or marked the testing section as N/A): SILENT HILL: The Short Message (PPSA10112), rooftop/title scene and stationary first room; original combined audit was synthetic-only.

…ostics switch is on

The history was always on: every deferred write built a kind string, took an interlocked
slot and a timestamp, and a DMA copy read the guest source only for the record. Record is
now a branch while SHARPEMU_CP_HISTORY, SHARPEMU_CP_WATCH or SHARPEMU_GPU_CLOBBER_DIAG is
unset, and the call sites skip the string and the guest read. The stall, clobber and
shader-code dumps read the history when it is on. Tests enable it for the whole run.
…read scratch

Each draw allocated a read buffer, a widened array, a List, its copy and the output bytes
(about 30 B per index, LOH above ~21k indices). The passes now read into and write from
reused per-thread buffers (grown to the largest draw, not retained above 8 MB) and the
upload takes exactly the byte count. Output is checked equal to the previous allocating
forms in IndexExpansionTests.
… has happened since

SHARPEMU_REGISTER_SNAPSHOT_REUSE=0 takes a fresh snapshot every time.
…polls watched labels from 50 us

SHARPEMU_CP_QUEUE_PULSE=0 and SHARPEMU_CP_WATCH_ADAPTIVE=0 restore the old behaviour.
… SHARPEMU_SPLIT_STATS

Every guest address translation from the worker, the program resolvers and the ring threads did a plain shared _hits++ or _misses++, one cache line bouncing between cores per descriptor word, shader read and staged range. The counters are now off unless SHARPEMU_SPLIT_STATS=1 (CountCacheAccesses); the existing hit/miss test turns them on.
… of them

The call tables' service bell woke every sleeper (8 threads in the GPU process, 4 in the guest process); all but one found nothing, spun 50/20 us and slept again. Doorbell.RingOne wakes one waiter; the item is published before the ring, waiters re-check after registering and a woken server drains the table, so no wake is lost. Test: 400 sequential calls with 8 sleeping servers caused 2691 empty wake-ups before and fewer than one per call after; a 6-caller x 3000 call contention test never loses a call.
…e waits once for its protections

Interest: the summary in shared memory had one bit per 64 KiB, so any managed write next to a watched page (command buffers sit beside the images they draw to) made a blocking CpuWrite call of 4-25 us plus a ring record. It is now one bit per 4 KiB page, set and cleared by B's page watch transitions (32 MiB of sparse address space). SHARPEMU_SPLIT_STATS=1 adds ManagedWriteInterest, ManagedWriteCall (timed) and ProtectWait (timed) to the [SPLIT-X] line.

Protections: every watch protection that takes rights away waited for A's acknowledgement once per contiguous run per 4 MiB block, with the page guard's block lock held. PageGuard now brackets a watch change with IProtectionBatchSpace; the GPU process's address space defers the waits to the end, outside the locks, and waits once for the last request (the counter is cumulative). A change that found its pages already watched by another thread waits for that thread's pending number too. SHARPEMU_SPLIT_BATCH_PROTECT=0 restores a wait per protection.

Tests: page-granular summary (a write to an unwatched page in a watched 64 KiB group is not of interest), batching (one wait for a nested batch, for a sparse 3-run mask, and for another thread's pending protection).
…ncoder residency they already resolved

Argument buffers, push data and the uploaded flattened/shader-data tables are filled in host
scratch and found again by content in a per-submission memo (MetalRegionMemo); a range is only
reused by the submission that wrote it, so a ring still in flight is never overwritten or handed
to a later submission. Guest window resolves answer from the window the submission last placed a
range in (keyed on serial, partition version and trap version; single-copy releases, holes and
stale windows take the full path). The submission resident list is named once per encoder and list
version instead of walked every draw, and a stage's memoised textures are confirmed under one image
cache hold instead of one per texture.

Measured (e2e 8a-8f, x64 and arm64, machine load 20-140 so only the quietest run is trusted): x64
8e 7486 -> 7075 ns/draw, 8d 4399 -> 4358, 8c 7535 -> 7429, 8f/8a/8b within noise; B/op unchanged
(1393 / 3720). arm64 runs were not separable from noise.

Switches (default on, =0 turns off): SHARPEMU_METAL_V2_ARGUMENT_REUSE, SHARPEMU_METAL_V2_WINDOW_MEMO,
SHARPEMU_METAL_V2_RESIDENT_WATERMARK. Verify mode via SHARPEMU_PERF_VERIFY=1.
…on by the descriptors they point at

ResourceMaterializationCache keyed a draw on every user data word its plan reads, so a
descriptor-table pointer that moves every frame (a per-frame descriptor ring) put the same
object under a new key every frame. The address rebase could take the pointer out of the key,
but it learned the pointer only by comparing a draw with the latest draw of the same plan,
which is another object whenever a shader draws several materials: it never confirmed, and
every draw was a key-not-found miss and a full walk (the game's 62% key-not-found).

The rebase now keeps, per plan, the latest entry per content print as well: a hash of the
descriptor dwords the plan loads through each raw pointer candidate (flattened table slots
resolved, buffer base dwords left out because memory-loaded bases move every draw). Learning
compares a draw with the same object's previous draw through another pointer, so the pointer
confirms after a few draws. Once it is rebased, the key holds the print of the descriptors the
pointer points at now, read from guest memory, so objects drawn through the same moving
pointer keep separate keys and the same object finds its entry wherever its table lies. The
print only chooses the key; Entry.Matches and Validate (every recorded range re-read at its
moved address and compared) still decide every hit, so changed descriptor words miss or
refresh as before.

A hit through rebased or differing user data built a new ResourceSnapshot (and patched
buffer copies) unless it repeated the entry's single last hit. Each entry now keeps its last
8 hit snapshots, immutable and shared, so draws cycling through a few pointers allocate
nothing; a snapshot handed to a draw is never written afterwards.

Switch: SHARPEMU_RESOURCE_CONTENT_KEY=0 turns the print (key and learning partner) off.

Microbench (run-bench.sh, machine heavily loaded: load average 100-240, so ns are noisy;
hit rates and B/op are exact):
- new 3g, 32 materials/frame through a drifting descriptor ring, same contents each frame:
  hit rate 0% -> 100% steady (74% over the first 8 frames); 9,181 -> 856 B/op (688 of it is
  the bench's WriteTable); arm64 6,588 ns (quiet base) -> 1,069-1,481 ns (loaded);
  x64 20,364-30,249 -> 1,158-2,060 ns.
- new 3f, 50 draws over 8 tables with identical contents: hit rate 92%/100% before and
  after; 127 -> 64 B/op (64 B is the bench's delegate, the cache now allocates 0).
- 3a-3e and 8a-8f: B/op unchanged (3a 3,152, 3b 64, 3c ~8.2 K, 3d ~8.4 K, 8a-c 3,720,
  8d-f 1,393 producer-side); 8e sampled allocations by type lose ResourceSnapshot
  (320 B/draw sampled on base, none after). Timings within the load noise.
…s; single-copy retile reuses its staging buffer

Staged range cache: one CPU store used to drop the watch on the whole entry, so the next bind
copied the whole entry (up to 64 MB) and re-added a watch on every page. For entries of 1 MiB or
more, a fault or a precise managed write now marks only the 4 KiB pages it touches (bitmap per
entry), drops the watch on those pages, and leaves the rest current and watched. The next bind
rewatches the dirty pages and copies just them (and their pristine twin) in place. Falls back to the
old whole-entry stale for: GPU/host writes, shader stores never written back, a writeback with
unwindowed bytes, an entry a submission still reads, an entry with no pristine twin when a written
binding needs one, and more than 32 dirty pages (then the next 8 stagings drop the whole watch on the
first store, so a wholesale writer pays at most 32 faults once per 9 cycles).
Switch: SHARPEMU_METAL_V2_STAGED_CACHE_PARTIAL=0 restores the whole-entry behaviour.

Single-copy restore: the retile staging buffer is taken from a small pool (4 buffers, 256 MiB, 1 MiB
size classes) instead of a fresh newBuffer per restore on the faulting guest thread. The zero fill and
the wait are kept. Switch: SHARPEMU_SINGLE_COPY_RETILE_POOL=0.

Synthetic bench (64 MB entry, one 4-byte store, rebind; noisy shared machine, load 100+):
read-only, in place: 6.7-13 ms and 64 MiB copied -> 40-53 us and 4 KiB copied;
written (with pristine): 15-20 ms -> 36-49 us. Watch churn per rebind 16384 pages added and removed
-> 1 page. Not changed: PageGuard block lock across mprotect, and the written-binding double copy.
…en the guest does, and resolve global records once

GPU time of translated shaders was ~200 ms/frame in Demon's Souls (audit finding A2).
Three changes, each A/B-timed on the device with MTLCommandBuffer GPUStartTime/GPUEndTime
(MslGpuShaderTimeBenchTests, opt-in SHARPEMU_GPU_BENCH=1, same process, best of runs):

* Math mode: libraries compile with MTLMathModeRelaxed + precise library functions instead
  of safe. ALU loop (fmac, mul+add, sqrt, rcp, sin): 9.3 -> 8.3 ms (-10%, quiet run);
  10.1-11.2 -> 8.9-9.2 ms under load. Relaxed treats signed zeros as equal, which broke the
  omod "flush zero to +0" test (x == 0.0f); it now tests the bits. The RDNA2 ALU
  conformance suite and the numeric tests pass. SHARPEMU_MSL_MATHMODE=safe goes back. The
  mode is part of the binary-archive library key and the prewarm stamp, so safe-mode
  binaries are never reused.
* Early Z: a pixel function gets [[early_fragment_tests]] when DB_SHADER_CONTROL.Z_ORDER is
  not LATE_Z and the shader has no kill, depth, stencil or coverage export. The existing
  PixelInputInfo.EarlyDepth (already in the stage key) now also covers RE_Z and
  EARLY_Z_THEN_RE_Z and reaches the translator as ShaderCompileRequest.PixelEarlyDepth.
  8-layer overdraw of a buffer-storing fragment function: ~34 ms -> ~4.0 ms (synthetic
  upper bound). SHARPEMU_MSL_EARLYZ=0 leaves it to Metal.
* Device spans: GLOBAL_LOAD/STORE_DWORDX2-X4 resolve the record once (page-crossing or
  unmapped records fall back per dword). Compute loop of global_load/store_dwordx4:
  6.8 -> 5.2 ms (-24%). SHARPEMU_MSL_DEVICESPAN=0 resolves per dword. The s_load span and
  the one-entry range cache were already on main; descriptor-addressed buffer accesses
  place each dword through the runtime swizzle, so they stay per dword.

Edits outside the owned files: ShaderCompileRequest.PixelEarlyDepth, ShaderProgramCache
(one line), PixelStageInputResolver (Z_ORDER != 0), MetalBinaryArchive.LibraryKey,
MetalShaderPrewarm stamp, test harness MetalNative compile options.
…get once and take no lock on a cache hit

Per-draw shader and pipeline resolution on the GPU worker made one guest read per pixel input
(up to 32), two per vertex attribute, and a hash of the whole code for a shader without a
declared hash, and resolved each bound color target (a several-hundred-byte request) up to
four times. Each guest read takes the memory read lock.

- PixelStageInputResolver reads the semantic words in one read.
- VertexInputResolver reads the attribute words and the buffer descriptors in two reads and
  keeps the decoded layout with the bytes it came from; equal bytes at the same tables share
  the previous draw's attribute and buffer arrays, any other byte decodes afresh.
- ShaderIdentity remembers the hash of a shader without a declared hash with a copy of its
  code, validated by an in-place compare (ICpuMemory.TryCompare) instead of read plus XxHash3.
- ShaderPipelineCache resolves each bound target once per draw and passes the slots and
  extents to the dual-source, render-area and output steps.
- The pipeline maps and the header registry's record map are concurrent: a hit takes no lock.
  The resolve gate and the program gate stay, they guard the program cache's mutation.

Direct measurement (ShaderPipelineCache.GetGraphicsPrograms, vertex program with 3 attributes,
pixel program with 4 custom inputs, 4 color targets, no declared hash, arm64, thread CPU time,
host load ~180 so absolute numbers are inflated, A/B interleaved): 3.97 us -> 2.40 us per draw,
1368 -> 744 B per draw, 20 -> 7 guest reads (+2 in-place compares) per draw. The audit's 2a-2d
and 8a-8e rows do not move: their rig has no attribute tables, no pixel semantics and declared
hashes. No behaviour switch: the caches validate against guest bytes.
…e QoS threads; exit ends the session; thread-time and memset

usleep/nanosleep/sched_yield/scePthreadYield: once a thread had polled for 500 us every
sub-ms sleep slept max(requested, 250 us), so back-to-back usleep(100) cost ~290 us (the
game's GPU-label poll). Requests at or above SHARPEMU_USLEEP_NAP_FLOOR_US (50) are now
honoured; tiny requests and yields nap from the floor, rising linearly to
SHARPEMU_USLEEP_NAP_US (250) over SHARPEMU_USLEEP_NAP_RAMP_US (4000). Existing env names
unchanged; SHARPEMU_USLEEP_NAP_LEGACY=1 restores the old fixed policy.
Measured (test eboot, usleep(100) back-to-back p50): 288-298 us -> 130 us; threadsim d
streak 320 -> 128 us; threadsim b usleep(100) poll start p50 553 -> 388 us, p99 1820 ->
1096 us, idle worker CPU 0.19 -> 0.51 cores (13 workers, the cost of really sleeping 100 us).

QoS: CommandStreamWorker and the CLI emulation thread now start as native HostQosThread
(user-interactive) threads; pthread_set_qos_class_self_np returns EPERM on CoreCLR
threads. CommandStreamWorker.WorkerQosClass reads the class back (0x21, tested).
Managed children of a native thread get no class (measured), so guest workers are
not promoted.

exit()/_exit() end the host session (SHARPEMU_EXIT_TERMINATES=0 restores the old
behaviour); the emulator now exits by itself after the guest's exit().
Adds sceKernelGetThreadTime and pthread_getcpuclockid (calling thread). Large memset
fills the mapping in place (managed export 18-20 -> 24-30 GB/s).
…ers nor any title's imports name it

The export was added with a NID derived from a guessed name. Demon's Souls and
Grand Theft Auto V import neither it nor pthread_getcpuclockid, and only the
latter is declared by the SDK (pthread.h), so only the latter stays. The host
thread-CPU-time helper it used has no other caller and goes with it.
…host-thread pthread_self, one thread-state object, in-place large memset

Test eboot (osx-x64 under Rosetta, warm pass, host load ~120 so compare same-session pairs;
logs w2-before2/3 vs w2-after1/2, w2-agc-*):
- clock_gettime(MONOTONIC) 300/231 -> 16 ns; sceKernelClockGettime 500 -> 18; clock_gettime(REALTIME)
  298/239 -> 22-30; gettimeofday 305/237 -> 22; sceKernelGetProcessTime 157/108 -> 15-18
  (stubs, SHARPEMU_HLE_FAST_CLOCK=0 disables; same epoch/units as the managed exports)
- scePthreadSelf on the main thread 284/192 -> 3.2-3.6 ns (SHARPEMU_HLE_FAST_SELF_HOST=0 disables)
- generic crossing (getpid, GetCurrentCpu) 257/244 -> 167-220 ns: the ~25 thread-statics per crossing
  are one per-thread object read once, ImportCount no longer takes a locked add
- AGC builders through the real dispatcher (w2-eboot, SetShRegisterDirect/SetCxRegisterDirect/DrawIndexAuto/
  DrawIndex/CbDispatch/EventWrite): 320-360 ns full path -> 220-235 ns leaf, DcbWriteData 367-440 -> 313-326
  (SHARPEMU_AGC_BUILDER_LEAF=0 disables). A leaf never calls guest code: a full buffer declines before
  anything is written and the full path repeats the call; tests compare leaf and full bytes per builder.
- memset >= 64 KiB fills the guest range in place (ICpuMemory.TryFillInPlace) instead of staging 16 KiB
  chunks: hle_memset_32MB 6-12 GB/s -> 12-21 GB/s warm (SHARPEMU_MEMSET_INPLACE=0 disables)
- lazy trace strings in the AGC builders, single guest write for register packets, AcbWaitRegMem and
  AcbEventWrite as one packet write
# Conflicts:
#	src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs
Every fiber switch in the process serialised on one gate, and the run path
re-read all fiber fields under it. Fibers now hash to 64 lock stripes; the
caller-continuation capture and previous-fiber checks run before the lock and
only the target's state word is read under its stripe. A source fiber in Run
is touched only by its own thread; its continuation is published before
Complete writes Idle under the source stripe, and runners read the state
under that stripe, so the next runner sees the continuation. Rollback takes
both stripes in index order.

New test: 8 workers x 1500 iterations over 6 fibers never run the same fiber
twice at once and leave every fiber Idle.

Switch: SHARPEMU_FIBER_GLOBAL_GATE=1 maps every fiber to one stripe.
…late lock

Every __tls_get_addr took the TLS template lock. Each host thread now caches
the resolved block address and size per module for its current thread
pointer, validated by an epoch that is bumped under the lock whenever a
module is registered, the init image changes, a thread block is seeded or
the template is reset; cache fills also happen under the lock, so a stale
entry can never pass validation. Out-of-block offsets still take the slow
path.

New test covers cached resolves, past-block offsets, a lazily allocated
block for a late module, re-seeding moving it into static TLS, and reset.
No switch (pure cache).
Every ULT mutex unlock, queue push/pop and condition signal woke every
blocked guest thread parked on any ULT object, each re-ran its predicate and
all but one went back to sleep. Mutexes and queues now park their waiters on
a per-object wake key, condition waiters on their mutex's key, and a release
wakes one waiter on that key: each release frees one unit and a woken waiter
consumes it in its predicate, so waking one cannot lose a wakeup. Joins keep
the shared key and wake-all semantics.

Covered by the ULT case of the scheduler wake stress tests. No switch (wake
targeting only).
…t through one shared stub

A cross-thread wake used to queue the thread, pulse the ready dispatcher, let
the dispatcher claim it and only then signal the thread's runner, and every
resume emitted, protected, ran and freed a fresh continuation trampoline.
Under Rosetta each fresh executable page is retranslated (~17 us p50 in a
standalone mmap/mprotect/call/munmap loop), which was most of a wake.

Direct handoff (F1): the waker claims the executor token under the scheduler
gate when the thread's executor is idle and signals only that thread's
runner; a releasing executor passes the token straight to its own next slice.
The park-time re-check is unchanged: a parking thread becomes Blocked and
then re-runs its wake predicate under the gate, and wakers only ready threads
that are Blocked, so a wake can neither be lost nor double-scheduled.
Otherwise the old queue + dispatcher path is used.

Shared resume stub: a one-time entry stub saves the host frame and jumps into
the existing context-transfer stub with a per-call data frame. Not used where
emitted code must swap stack bounds or for the native-worker path.

Also adds IGuestThreadScheduler.IsGuestThreadExecuting (lock-free) for the
adaptive mutex spin, and a scheduler test seam with wake stress tests
(semaphore producers/timed consumers, blocking ping-pong pairs, a contended
mutex whose owners park while holding it, ULT per-object wakes). With the
park-time re-check disabled all three scheduler stress tests fail.

Test eboot pass 2, p50 us, 3 interleaved runs each (host load 10-100):
  sema.N4.wake      50.1/99.4/30.3 -> 7.4/71.2/7.0
  sema.N4.roundtrip 77.6/49.5/32.0 -> 21.1/26.1/8.9
  cond.N4.roundtrip 70.1/69.6/33.6 -> 8.6/11.6/13.9
  evf.N4.wake       47.9/44.9/48.1 -> 7.3/8.0/8.0
  sema.N13.wake     59.7/23.2/26.8 -> 7.1/9.0/8.1
  sema.N13.fanout   892/329/327    -> 59/52/48
Stub off: N4 wakes ~21 us. Handoff off: N4 ~9-11 us, sema.N13.fanout 237 us.

Switches: SHARPEMU_DIRECT_WAKE_HANDOFF=0, SHARPEMU_SHARED_RESUME_STUB=0.
… at most 10 us

A queued waiter spun up to 40 us for its grant even when the owning guest
thread was parked, which burns a host core for nothing: a parked owner cannot
release the mutex within the spin window. The waiter now stops spinning as
soon as the scheduler reports the owner is not executing, and the bound is
10 us. Parking early is safe because the park path re-checks the grant under
the scheduler gate before the thread blocks.

Threadsim e (host threads, no guest scheduler, so only the bound applies),
total Mops/s over 5 runs: 4 threads 3.15 -> 2.62 mean, 13 threads 2.88 ->
2.92 mean; single-thread uncontended varies 2.1-4.7 between runs, so this is
within noise. Test eboot uncontended mutex pair is unchanged (467-991 ns
before, 480-1313 ns after, load-dominated).

Switches: SHARPEMU_MUTEX_ADAPTIVE_SPIN=0 restores the 40 us unconditional
spin; SHARPEMU_MUTEX_SPIN_US sets the adaptive bound.
…daptive mutex spin, striped fiber gate, TLS cache
…lock

The 50 ms limit counted from before the first packet read, so a first scan slowed by JIT or a busy host stopped before reading anything. PipelineLookaheadTests then saw no prefetched dispatches and failed intermittently on CI (run 40600561034). The scan now always reads its first 2048-word block before checking the clock; the word and dispatch caps are unchanged. Libs 8,901 passed / 122 skipped.
…READLANE_B32

Cause: each V_READLANE_B32 of a guest wave64 is its own threadgroup exchange (flip, owner
writes, barrier, read). The wave-scan idiom reads lane 31 and lane 63 back to back, so it
paid two barriers where one serves both: the owners write slots 2 and 3 of the same set.

Two consecutive reads from inline-constant lanes, both vector source to scalar destination
and neither a spill reload, now take one exchange. Everything else keeps one each.

Round-2 k21 items 1-6 and 7 (default off) were already in this branch; of the rest, the
get_num_mip_levels skip is in too, and this is the wave64 exchange item. Demon's Souls
compute dumps have 24-93 exchanges per shader, 4-6 of them readlanes, so the real effect
is small.

Measured (MslTranslatorOverheadBenchTests.FusedLaneReads, 256 trips of lane 31 + 63,
4096 groups of 64, GPU start/end time, best of 3, two runs): 2.099 ms -> 2.009 ms
(-4.3%) in both runs. Execution test on device returns each lane's values in both halves.

Switch: SHARPEMU_MSL_READLANE_FUSE=0 keeps one exchange per read.
…p table or a pause-jmp tail

The pass skipped any pause in a function whose jump table it could not resolve (clang hoists the
table base into a callee-saved register and bounds-checks with a 64-bit cmp, which the resolver
rejected) and any `pause; jmp short loop` (4 bytes, one short of the patch jump). A pure
spin-wait loop of that shape never napped, so 13 such workers plus the producer kept every core
busy and a loaded host starved the producer (test eboot group 3, "TEB TIMEOUT stage=3-sync-cross").

The spin pass now (only it; the red-zone pass is unchanged) trusts a hoisted table base when one
RIP-relative lea is the sole write of that callee-saved register in the function (the epilogue pop
is ignored), accepts a 64-bit bounds compare, and takes the alignment nops after an unconditional
jump to reach 5 bytes unless something branches to them. Off with SHARPEMU_SPIN_NAP_SHAPES=0.

Test eboot built with an eh_frame_hdr (the stock variants have none, so the pass never runs on
them), group 3 spin only, release bundles, 2 runs per arm, same session, host load average 11-35:
 quiet  N13.wake  rounds 2401-3291 -> 10000/10000, mean 2.4-3.3 ms -> 0.29-0.38 ms, p50 667-959 -> 583-667 ns
 quiet  N13.fanout rounds 734-830 -> 4544-5000 of 5000, p50 9.0-9.5 ms -> 0.84-1.0 ms
 quiet  N13 cpu_per_round 20.5-26.1 ms -> 2.7-3.5 ms
 12 yes N13.wake  rounds 292-340 -> 967-1253, p50 10-18.8 ms -> 1.4-1.95 ms
 12 yes N13.fanout rounds 103-125 -> 220-328, N13 cpu_per_round 67-98 ms -> 13-17 ms
 N4 wake/fanout p50 unchanged (208-209 ns / 375-500 ns); the nap adds tail (max up to ~0.33 ms,
 fanout p99 up to ~0.5 ms in 3 of 8 runs) as it does for any patched loop.
…tion's jump tables are resolved

A pause-jmp spin tail takes the alignment nops after its jump to reach the 5-byte patch. Those nops are dead only if nothing branches to them. With an unresolved jump table an indirect target is unknown, so the padding is left alone there. SHARPEMU_SPIN_NAP_SHAPES=0 still turns the wider shapes off.
…r a free slot instead of re-polling it.

Wwise's AK::EventManager thread loops: Perform (sink IsDataNeeded = sceAudioOut2ContextGetQueueLevel, render
the available grains, Advance + non-blocking Push), then sceKernelWaitEventFlag(flag, 1, OR|CLEAR_ALL, NULL,
&timeout) with a timeout that is 0 for the sample-rate/frame-size configuration the title ships. Once the
queue is full the level cannot change before the next grain period, so that loop re-polled the same answer
at over a hundred thousand times a second and held a core.

GetQueueLevel now records, per guest thread, when the queue it read as full gets a free slot (clock and
device paced), and the next timed sceKernelWaitEventFlag of that thread is lengthened to that time (at most
50 ms, once per read). A wait the flag ends early is unaffected, a longer timeout is kept, and the queue is
refilled the moment a slot frees. SHARPEMU_AUDIO_OUT2_IDLE_WAIT=0 restores the zero-timeout poll.

The test drives the Wwise loop against a context: 92 polls per second instead of 170603, and 52 ms instead
of 481 ms of thread CPU per second, with the queue kept fed.
…instead of a blit per copy

Silent Hill's first room holds about 19,000 label, occlusion-dump and command-processor
copies a frame, up to 16,887 at one encoder boundary, and encoding a copyFromBuffer per copy
took about 9% of the GPU worker (most of it AGX checking each blit against the earlier ones).

A batch of 16 or more copies now becomes a list of 32-bit word moves (device addresses) that
one compute dispatch performs. A word several copies write keeps the last copy's source, so
the result is what the in-order blits left. A batch with a copy that is not whole aligned words,
or in which one buffer is both source and destination, keeps the blits. The encoder waits on
and updates the encoder fence, and the label barrier precedes it, as for the blit encoder.

Synthetic device benchmark (arm64, median of 10 alternating rounds), worker encode / GPU land:
1,024 copies 733 -> 207 us / 1,396 -> 1,370 us; 4,096 copies 2,733 -> 551 us / 2,124 -> 1,430 us.
SHARPEMU_METAL_V2_LABEL_SCATTER=0 keeps the blits.
…guest process, hook by hook

The agc selector runs the real builder bodies over a PhysicalVirtualMemory whose GuestGpuMemory holds the split's page filter (a real SplitSharedState), and times the per-write hooks, the command-buffer reads and writes and whole draw sequences on their own.
…t write

Every managed sceAgc builder wrote the 8-byte cursor and then the packet through two
separate TryWrite calls, each paying the alias pin, the managed-write interval, the
history record and a full MarkCpuWriteLanded (Covers, MemoryBarrier, store checks).

ICpuMemory gains TryWritePair. PhysicalVirtualMemory implements it as two ranges under
one view lookup and one write operation, notifying the stores per range as before, then
landing both ranges with a single barrier (the interest-bit read still follows the
stores, so the GPU host's set-bit-then-read protocol is unchanged). TryEmitCommandDwords
and the reserve/commit helpers use it. SHARPEMU_PAIR_WRITE=0 restores the two writes.

The packets and cursor the GPU sees are byte-identical; tests cover both ranges landing
with the stores hearing of two writes and a failure when either range is unmapped. The
perf-audit agc selector gains a b5 bench for the pair write.
…e guest

For 14 leaf builders whose whole effect is "append these dwords at the command
buffer's cursor and advance it" (register sets, index state, draws, dispatch) the
guest-side import now jumps to a generated x86-64 stub instead of crossing into the
managed export. The stub reads cursor/end/reserve from the 0x24-byte buffer state,
stores the dwords with plain guest stores and bumps the cursor. Plain stores are what
the title itself would do, so write-protected GPU-interest pages are still caught by the
fault handler. It declines to the normal trampoline for a null buffer, an inverted
buffer, or without room for the packet plus the buffer's reserve, which is exactly where
the export would run the buffer's callback.

Gated like the other stubs (native thunks allowed, write-watch armed, no AGC tracing).
SHARPEMU_AGC_PACKET_STUBS=0 keeps every builder on the managed exports.

AgcPacketStubTests runs each stub as machine code against its export with randomized
arguments and buffer room and compares bytes, cursor and return value.
…sDirect in the guest too

These are the two per-draw user-data builders left after the fixed-shape packet stubs:
RangeDirect (offset, values pointer, count) and RegistersDirect ((offset, value) pairs).
Each gets a guest-side x86-64 stub that copies the payload into the command buffer with plain
stores and advances the cursor, on the same terms as the fixed-shape stubs (decline to the
import trampoline with the arguments intact).

RangeDirect: offset 1..0x3FF, 1..64 values, non-null source that does not overlap the packet,
room for marker NOP + SET_SH_REG + values + the buffer's reserve; returns the SET_SH_REG
packet's address exactly as the export's in-place path does.
RegistersDirect: 1..32 pairs whose offsets are consecutive ascending, where the export's sort is
a no-op and it emits one packet. The pairs are verified before anything is written, so a decline
leaves the buffer untouched; anything else (unsorted, runs, large) is the export's.

AgcPacketStubTests runs both as machine code against their exports over randomized counts,
offsets, room, reserves, overlapping and null sources, and checks bytes, cursor and return
value, and that a decline never touches the buffer. SHARPEMU_AGC_PACKET_STUBS=0 turns them off.
…re on

BeginRendering formatted "render WxH" for every pass and LabelEncoder then dropped it unless SHARPEMU encoder status labels were enabled: about 0.25% of the Silent Hill GPU worker's samples. The string is now built only when it is used.
TryTakeGraphics read the clock before and after every take to account the
render thread's wait, though a completed job (nearly every take of a
well-prefetched burst) waits for nothing. Only a job still queued or
running reads the clock now; the wait and caller-resolve counters are
unchanged.
…s that found nothing last time.

DeferredLabelCopiesTouch remembers, per guest range, how many held label copies a scan covered and resumes after them, since the copies are append-only until ClearHeldLabelCopies. MetalGpuWriteRanges.PendingSerial remembers 256-byte granules no live record or label overlaps, dropped when Note writes into them and when a fold merges records. Both are on by default; SHARPEMU_METAL_V2_LABEL_SCAN_MEMO=0 and SHARPEMU_METAL_V2_WRITE_CLEAN_MEMO=0 turn them off.
… one run instead of once per block.

A dump of 16 blocks resolved a window, forgot the overlays twice, invalidated images and
noted a GPU write sixteen times, and left a write record per block. It now resolves one
window over the run, forgets only each block's own bytes in the overlays (a pending store
between the blocks still speaks for them), and invalidates and notes the run once.
SHARPEMU_METAL_V2_COALESCE_OCCLUSION_DUMPS=0 restores the per-block path.
…nd dump cost, and worker stream cost.

SHARPEMU_INTERPRETER_BENCH_OUTPUT (Libs tests) times each packet type and a frame-shaped
stream through a null host; SHARPEMU_NONDRAW_BENCH_OUTPUT (Metal tests) times labels and
dumps through the render host rig; SHARPEMU_WORKER_FRAME_BENCH_OUTPUT times draws with
dumps, labels or writes after each through the whole path. Each returns at once without its variable.
… the unsynchronized copy only when tracing.

ApplyRegisterTable zeroed two 1 KB stack blocks per table packet. The block is now sized to
min(count, 128) entries and the second exists only for SHARPEMU_GPU_TRACE_REGISTER_TABLES.
Null-host bench, ns per packet (best of runs): 24-entry context table 180 -> 170,
16-entry shader table 137 -> 124.
…ro-fill-then-dump variant, and an open-batch dump case.

The open-batch case in the Metal rig opens a visibility batch by hand so dump copies are held
as they are behind a render pass; the stream variants time the added packets per draw.
…roseconds per draw per variation.

SHARPEMU_DRAW_RECORD_BENCH_OUTPUT=<json> runs one 256-draw PM4 stream (three sampled textures, depth, two vertex
buffers) in five variations: the same state, an alternating texture, sixteen alternating pixel programs, a render
pass every four draws, and all three mixed. It reports wall and recording-thread CPU time per draw, and the
SHARPEMU_PROFILE_RENDER self time per phase when that is on. Returns immediately without the variable.
… it counts.

PipelineVertexInputState.Equals compared the whole 32-entry arrays; their records are padded structs, so every
per-draw pipeline map hit compared 64 mostly-empty records one by one (32 of the 87 samples in
ShaderPipelineCache.CreateGraphicsPipeline on the Silent Hill worker). It now compares the counted prefix, as
GetHashCode already hashes it. The per-draw lookup of a 300-pipeline map drops from about 120 ns to about 106 ns
(GraphicsPipelineKeyTests.MeasurePipelineKeyLookupWhenRequested, arm64, thread CPU time).
SHARPEMU_DRAW_RECORD_BENCH_TEXTURE=<edge> sets the edge of the three sampled images (8 by default). Edges of 512 and
1024 change the recording cost by less than the run-to-run noise, so the draw path does not scale with image size.
…ds an occlusion dump after every draw.

SHARPEMU_DRAW_RECORD_BENCH_FILL=images=,windows=,overlay=,writes=,dccfills= fills the cached images, the guest windows the draws fetch from, the pending command-processor writes, the GPU write ranges and the recorded DCC fills before the measured rounds. SHARPEMU_SCENE_MEMORY_MB sizes the scene's block (mapped anonymously above 64 MB) so a few hundred windows fit, and ..._DRAWS sets the draws per submission. The new Production variation is Mixed plus a dump per draw and a label every eighth.
…cating and moves the overlay version only when an entry changed.

The Dead array every partly covered entry allocated is a bit mask for entries up to 64 bytes. The three forgets that a GPU write range note follows (a store, a copy run, an occlusion dump) bump the overlay version only when they removed or narrowed an entry; host writes keep the unconditional bump, since nothing else tells a cache of memory a host store happened. SHARPEMU_METAL_V2_OVERLAY_FORGET_VERSION=0 bumps on every forget again.
…of by a walk over all of them.

Every GPU store and every slice lookup enumerated the whole recorded-fill dictionary; a fill-start page count plus the largest fill size now decides that a store reaches none. SHARPEMU_METAL_V2_DCC_CLAIM_INDEX=0 walks them all again.
…nt list instead of rebuilding it.

The list was rebuilt, and every buffer in it named again on the encoder, at each of the few hundred first touches of a submission. The list now only grows while its version holds and an encoder is told only the buffers added since its last draw. SHARPEMU_METAL_V2_RESIDENT_APPEND=0 rebuilds on every touch again.
… forms of the same work.

Standalone compute micro-kernels (exec guards, mul+add contraction, V_MIN/V_MAX
sequences, f16 packing, LDS indexing, bounds-checked buffer loads, device-address
resolves) run with the translated-guest compile options; GPU time is the median of
interleaved command-buffer GPUStart/End samples. SHARPEMU_MSL_PATTERN_BENCH_OUTPUT=<json>.
…ro instead of branching around the load.

sharpemu_load_dword and sharpemu_load_dwords (device and constant copies) always issue
the load and select 0 for an index past the end, which answers what the per-dword bounds
check answered, wrap included. Without the branch the compiler batches a shader's buffer
loads: uniform four-dword loads took about a quarter of the time in the micro-kernel
(387 -> 93 us), per-lane runs about two thirds (1939 -> 1277 us). SHARPEMU_MSL_BRANCHLESS_LOADS=0
keeps the branching helpers, byte for byte the old goldens.
… with synthetic bindings.

mode=gpu builds each program of a title's recorded lists, binds a synthetic argument buffer,
buffers, textures and device-address page table the way Metal v2 does, and times a fixed
dispatch or draw per shader on the GPU, with compile-option and translator variants (and MSL
dumped from another translator setting, alts=) interleaved in one process; check=1 hashes the
written buffers and targets so two variants can be compared for output. mode=gpureport ranks
the results. Zero or patterned inputs change control flow, so the times are a cost proxy.
…o 512 buffers instead of 32.

A command buffer's held occlusion snapshots read from one result buffer per render pass, so a submission of more than 32 passes declined the scatter and landed every copy as its own blit (about 5 us per draw in the production bench variation). Buffer lookups remember the last hit, since consecutive copies share their buffers.
…raining them.

A drain at every dump left the program resolver one draw of lead, so each draw resolved on the worker (about 1.2 us of waiting plus 2.3 us of caller resolution per draw with a dump after every draw). The dump now takes the same ordered slot a deferred barrier does, runs in issue order at the next drain, and keeps the draws batched. Dumps posted from the command thread still record where they stand. SHARPEMU_METAL_V2_DEFER_OCCLUSION_DUMPS=0 drains at every dump again.
…nce instead of once per dump.

InitializeOcclusionSlot sent a contents message for every dump; a shared buffer's pointer holds for its life, so the batch keeps it from creation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant