Repository navigation
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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=0retains 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:
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
4b6bad96b38cabfc82c936efd0f561572b02deb9x64 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=0retains 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 addsc5ae56c56(CPU-clock correction) and4b6bad96b(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.
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 showedCLOCK_THREAD_CPUTIME_IDreturning 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=1opts in; unset or0keeps the allocating path.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
=0fallback. 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=0disables 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
fb717072aself-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 forfb717072a. 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
b4cc8292eruntime 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:eabcd9f65The 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
b4cc8292efailed 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.
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=0measured 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.
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.
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.
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
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
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
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.
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=0restores 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:
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 thesame 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/1differs.Both final arms disable shader prewarming to remove background-compilation work.
Only Silent Hill was launched; other apps remained open.
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
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.
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.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
CONTRIBUTING.md, with the explicitly requested combined scope.N/A.N/A): SILENT HILL: The Short Message (PPSA10112), rooftop/title scene and stationary first room; original combined audit was synthetic-only.