mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
8bc7ca307c2927a33456e89727a5bfe13132eeea
17179
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
55a54c924e |
SPU/Android: stop generated SPU code running off the end of the thread stack
The ARM64 SPU gateway reserved a shared 8192-byte stack scratchpad. Compiled SPU functions build no frames of their own on ARM64 -- GHC_frame_preservation_pass runs with use_stack_frames = false -- so every one of them spills into that single reservation, and a function needing more simply writes past it. Borderlands 2's 2401-instruction function at LS 0x25da8 wants ~21 KB: the fault landed at sp+21760, exactly the top of the thread's stack mapping, on the PROT_NONE guard page above it. x86 reserves 0xc8 in the same place because LLVM emits ordinary per-function frames there, so this arrangement and this failure are ARM64-only. Raised to 256 KB. That is still a fixed bound rather than a scaling fix; a larger function could overflow it the same way. use_stack_frames = true would scale, at a cost the pass comments call out and which is not measured here. Android threads also ran on an eighth of the stack they get elsewhere: the pthread path passed null attributes, so bionic's 1 MB default applied where glibc gives 8 MB, measured as a 0xfc000 stack mapping. Not the cause of this bug -- the overrun is off the TOP of the stack, so size does not affect it, and 1 MB to 64 MB changed nothing -- but a real discrepancy worth closing. Both were invisible because of how the fault died. A guard page is not emulator memory, so is_emulator_fault() correctly declines it, the handler forwards to libsigchain, and ART's FaultManager reads the guest registers as an ArtMethod* and takes the process down. No tombstone is produced, the async emulator log never reaches disk, and Android records only 'SIGNALED status=11'. Verified with the function compiled and no forced interpretation: zero stalls, zero guard-page faults, 47 presented frames where the previous best was 18. |
||
|
|
19d23eb691 |
SPU LLVM: fix ARM64 SHUFB byteswap fold and accurate-xfloat CFLTS
SHUFB:
|
||
|
|
884cb47dde |
SPU: fix ARM64 float-to-int conversions in the interpreter
CFLTS and CFLTU both carried x86 corrections that are wrong on AArch64, and the SSE templates they live in are what spu_interpreter_rt is built from, so they are live on ARM64 through spu_run_interp_fallback. CFLTS applied the cvttps2dq fixup: x86 returns the integer-indefinite value 0x80000000 for anything unrepresentable, positive overflow included, so the result was XORed back. _mm_cvttps_epi32 is sse2neon's vcvtq_s32_f32 (FCVTZS), which already saturates, so the correction inverted a correct result. Measured: +3e9 gave 0x80000000 instead of 0x7fffffff, and NaN gave 0 instead of 0x80000000. CFLTU went further and relied on the 0x80000000 return, ORing the remainder back in to rebuild the u32. On ARM64 the conversion yields 0x7fffffff, and 0x7fffffff | v is 0x7fffffff for every v below 2^31, so the entire upper half of the range collapsed to one value: 3e9 read back as 0x7fffffff rather than 0xb2d05e00. This also matters for diagnosis, not just correctness: forcing a block to the interpreter is the standard test for whether the recompiler emits wrong code, and until now that test could introduce a fault the recompiler did not have. |
||
|
|
9b33316982 |
SPU: copy the reservation line 16 bytes at a time on ARM64, and add SPURS dispatch diagnostics
mov_rdata and mov_rdata_nt move the 128-byte reservation line -- the GETLLAR snapshot, and the fill back into live guest local store. On x86 that is four 16-byte vector moves, so each quarter lands whole and a racing reader sees either the old or the new 16 bytes. On ARM64 both fell through to std::memcpy, whose granularity is a libc implementation detail; AArch64 implementations mix transfer sizes freely, so a reader can observe a line stitched from both versions. Use eight vld1q_u8/vst1q_u8 pairs to match what x86 gets for free. This does NOT fix the Borderlands 2 hang -- measured, no change to any observable: same 4807 SPU blocks, same 0x29b48 ceiling, same stall state. It is committed as a latent correctness fix rather than a behavioural one: the copy exists to produce a coherent snapshot and had no atomicity guarantee here at all. The diagnostics are the instrumentation that traced that hang from symptom to a single missing DMA: guest thread and thread-group state at an RSX stall, per-SPU conditional-store counters, the local-store and reservation-vs-memory dumps, code GET destinations, the SPURS control-block fields, and the register dump at the last transfer both hosts issue in common. They hang off the existing rate-limited stall report or are capped by distinct key, because every earlier attempt at this was capped by volume and got eaten by whichever event happened most often. |
||
|
|
55a35b5e1d |
Diagnostics: guest-thread stall reporting, SPU reservation counters, autotest harness
Hangs where the RSX idles were only ever visible from the RSX side, so a stall report now names every guest thread, its state, PC and function, and for SPUs adds the reservation counters -- conditional store calls, failures, notifications, and the SPURS heuristic's deliberate non-notifications -- plus where the host thread last was in cpu_task. block_counter alone cannot separate a thread livelocked retrying PUTLLC from one that is genuinely idle; both report zero blocks a second. The SPU code window prints once per process. Unguarded it re-emitted a whole function on every stall dump, measured at 538 lines a second over 31 dumps with a 690 MiB log left behind, which on Android is itself a stall -- it was degrading the hang it was meant to describe, and it buried the state lines that answered the question. do_local_task counters cover the case the profiler cannot: it reports the thread is in Local task and has been for 0.00s, which together mean it is not stuck there at all and the FIFO loop is calling it repeatedly. Which FIFO state, and whether guest GET equals PUT, separates a starved RSX from a stuck one. tools/ps3autotests drives ps3autotests on a device over adb and diffs per instruction against real-hardware output; compare-platforms.py does the three-way ARM/x86/hardware split that separates shared upstream failures from ARM-only ones. This is what found the CFLTS and FMS divergences. |
||
|
|
8caacc8231 |
Android: correct persisted off-spec settings, file:// launches, and RSS reporting
Accurate SPU Reservations was persisted false in the global config, left over from earlier debugging, where upstream and our own defaults are both true. Turning the default back on reached nobody who had already run the app, so this migrates the stored value -- correcting the curated field and forgetting the raw override at global scope only, since a per-title exception exists on purpose and forgetEverywhere() would take it with it. Save LLVM logs had the same problem and needed the value recorded, not just the override un-pinned. A file:// launch never booted: the intent path was passed through as a URI string and the loader wants a filesystem path, so only content:// ever worked. get_memory_usage() reports system-wide totals -- MemTotal minus MemAvailable, every process on the machine plus page cache -- and was being read as if it were ours. Add get_process_memory_usage() for this process's resident set, which is the number Android's low-memory killer actually decides on, and report that instead. |
||
|
|
424514fde6 |
SPU: fix two ARM64 float divergences and make the object cache key cover codegen
CFLTS applied an x86 saturation correction on every host. cvttps2dq returns the integer-indefinite value 0x80000000 for anything it cannot represent, positive overflow included, so XOR-ing all the bits when the input is >= 2^31 produces the 0x7fffffff CFLTS wants. AArch64's FCVTZS already saturates that way, so the same XOR turned a correct saturated-high result into saturated-low, and its NaN-to-0 conversion became 0xffffffff where x86 lands on 0x7fffffff. Same shape as the FCTIW/FCTIWZ/FCTID split already guarded in PPUTranslator; the SPU one was missed. FMS expressed a * b - c as fma(a, b, -c). x86 folds that into vfmsub and never materialises -c, so a NaN addend propagates its own bits; AArch64 cannot take that shape -- FMLS is Zd - Zn*Zm -- so it emits the FNEG and propagated the negated NaN. 0x7fffffff is not a NaN on a real SPU, just a large number, so the two hosts disagreed about the sign of a huge result. Negate the addend only when it is not a NaN pattern, with a known-never-NaN early out to keep it off the common path. Measured with ps3autotests cpu/spu_fpu against x86 output from an otherwise identical build: cflts 16 -> 0 differing lines, fms 484 -> 0, and spu_fpu as a whole 984 -> 0 against a non-AVX512 x86 host. The 484 fma lines that remain against an AVX-512 host are that host's vfixupimmps path and reproduce on any x86 without AVX-512, so they are not ARM-specific. The cache key hashed the build stamp of SPUCommonRecompiler.cpp while the code generator lives in SPULLVMRecompiler.cpp, so editing codegen alone did not move the key and a rebuilt emulator silently reused objects from the previous binary -- the first attempt at the CFLTS fix looked like it did nothing for exactly that reason. Export a stamp from the codegen TU and hash that in as well, and prune stale spuobj-* siblings so a version bump does not strand old directories. |
||
|
|
301f45a2cb | Merge PR #63: cellAudio: don't let a silent port reset the untouched baseline every period | ||
|
|
aa25da4ce2 | Merge PR #62: Stop two guest polling loops from flooding the log | ||
|
|
89c6d08ed3 |
cellAudio: don't let a silent port reset the untouched baseline every period
A game can leave an audio port started and write nothing but zeros into it.
Those writes still land on the tag slots, overwriting the -0.0f tag with
+0.0f, and count_port_buffer_tags() detects that sign flip as "the buffer was
touched" -- correctly, since it cannot tell silence from data.
The result is a port that reports untouched on most periods and touched on the
few that a write happens to land in. Storing untouched_expected as the
instantaneous count then drops it to 0 on exactly those periods, so on the
next period the same silent port looks like a newly untouched buffer, and the
loop waits out the whole untouched timeout for it. Every time it flickers.
untouched_expected is now a high-water mark, clamped to active_ports so a port
going away lowers it again.
Measured on device, Tom Clancy's H.A.W.X. 2 (BLES00928), main menu, stock
audio settings (time stretching off, buffer 34), with a temporary probe in the
period loop counting branch hits per second. Same scene, same build, only this
change differing:
before after
wait_untouched 669 0 hits/s (1000us each)
MIX 65 188 hits/s
advance (forced) 37 0 hits/s
enqueued_buffers 0 5-7
untouched > expected 743 0 per second
untouched_expected 0 in 799 1 in 376 of the second's samples
The port itself is unchanged by this: it is still started, still counted as
active, still mixed. A full-block scan of it reads 0 non-zero floats out of
512 on every one of 875 consecutive periods, which is what makes it silent,
and it is the tag flicker rather than the silence that caused the stall.
Audible effect: the audio clock ran at ~55% of real time (103 vs 189 periods
per second) with the ring buffer permanently empty, which is why the whole
title sounded slowed down and stuttering. Note this happens with time
stretching disabled -- the frequency ratio stayed at 1.000 throughout, so the
slowdown is the period rate itself and not resampling.
Not verified: whether any title depends on untouched_expected falling back to
a lower value within a stable port configuration. Nothing in the tree tests
this loop.
|
||
|
|
7952244052 |
Stop two guest polling loops from flooding the log
cellMicOpenEx logged at notice and sys_net_bnet_accept at warning, once per call. Titles poll both. In H.A.W.X. 2 they are called roughly 100 and 200 times a second respectively for the whole session, and together they were 46% of the log -- 27305 lines of 58441, about 9 MB per three minutes. On Android that file is on FUSE-backed storage, where writes are far slower than the f2fs the emulator's own data sits on, so this is not just noise in a text file. Neither call is an error. cellMicOpen and cellMicOpenRaw are thin wrappers around cellMicOpenEx and were already trace, so the wrappers were quieter than the function they call. A non-blocking accept() on an idle listening socket is a normal polling pattern, not a warning. After: 4 and 1 lines respectively, log down to 2.7 MB over the same span. Also corrects the heap-flag test in mem_allocator_vma: the loop checks a VkMemoryHeap::flags value against VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, which is a memory-type property rather than a heap flag. Both constants are 0x1 so behaviour is unchanged; this only puts the right enum on the test. |
||
|
|
7b49e1fcea | Merge PR #55: Android CPU time: park the dma_manager::sync() wait, and stop re-parsing the global config every 5s | ||
|
|
d9957c56ae | Merge PR #57: SPU: make the ARM64 uncompilable-block fallback safe to enter | ||
|
|
62d8208c71 |
VK: frame generation's Motion detail slider was inverted
framegen treats flowScale as a DIVISOR -- flowExtent = inputExtent / flowScale in v3.1_src/shaders/mipmaps.cpp -- which is why upstream's own layer passes 1.0f / conf.flowScale rather than the value itself. We passed value / 100 from a 25..100 setting, so every position below the default asked for a LARGER optical-flow pyramid instead of a smaller one: 100 -> 1.00 -> full resolution (correct, 1.0 being its own reciprocal) 64 -> 0.64 -> 1.56x per axis, 2.4x px 25 -> 0.25 -> 4x per axis, 16x px So a user turning "Motion detail" down to find speed got sixteen times the flow cost at the bottom of the range, and the slider got slower the further it was turned down. Only the default was ever right, which is why this survived testing. Now 100 / value. The ~10% of real framerate frame generation already costs is not this: that was measured before the setting existed, when the call site passed a hardcoded 1.0f. Anything measured since, at a non-default value, was carrying the inflated cost. |
||
|
|
cab4f2507c |
SPU: make the uncompilable-block fallback safe to enter
The failed-block set is consulted by two lookups that locate a candidate with upper_bound and step back exactly one entry, so they only ever examine a single range. That is correct only while no range can hide another, and nothing kept the set disjoint. The two marking call sites record different extents: one records a whole analysed program, the other records an entry point alone when there is no program to describe. An entry-only mark landing inside a program-sized mark is therefore ordinary, and it always ends first, which leaves the enclosing range invisible for every address past its end. mark() now merges on insert, so the invariant the cheap lookup depends on holds by construction. The set moves into spu_failed_block_set (SPUFailedBlocks.h), header-only and free of engine dependencies so it can be exercised directly rather than through a model of it. A hole was not merely a missed optimisation. dispatch armed the fallback with whatever the lookup returned, and old_interpreter releases the thread when (pc < begin || pc >= end), which is unconditionally true for an empty range, so the interpreter would return having executed nothing while dispatch re-entered at an unchanged pc. spu_arm_interp_fallback now yields a range that contains pc and is non-empty, recording the block first when no path had recorded it. It does that under one critical section rather than lookup, unlock, mark, look up again: nothing removes ranges concurrently today, so the gap was not live, but the guarantee rested on who happens to call the reset rather than on structure. It also recorded only [pc, pc + 4) while dispatch was holding the analysed program, so the interpreter released the thread after a single instruction and dispatch re-entered four bytes later to pay another full analyse and another full failed compile -- the 4-bytes-at-a-time walk documented at the top of this file. The extent is passed through when the caller has one. That path no longer logs "cannot be compiled on this backend" either: a null compile with no diagnostic also covers a poisoned engine, an analyser that produced nothing for a branch into data, and a lost compile claim, none of which are backend limits. The interpreter also ran in the wrong place. It was started from spu_thread::cpu_task after dispatch had escaped, which executes guest code outside any gateway invocation, while spu_runtime::g_escape resumes through the gateway epilogue whose address and stack pointer the prologue stored in hv_ctx -- belonging to a call that has already returned. A guest HALT, an MFC interrupt or cpu_work escaping from inside the interpreter would restore a stack pointer into a dead frame. It now runs from dispatch, inside the live gateway call. allow_interrupts_in_cpu_work is not restored after the old_interpreter call, because an escape out of the interpreter is a far jump to the gateway epilogue that abandons every frame in between -- a restore placed there is skipped on exactly the paths the flag is set for. Both that flag and interp_fallback are cleared by cpu_task before each gateway entry instead, which is the one point every escape returns through. interp_fallback was previously left set when old_interpreter exited through check_state() as well. spu_interpreter_fallback_available() tested spu_runtime::g_interpreter, the LLVM-built interpreter used when a recompiler is selected. The fallback actually run is old_interpreter, which reads the opcode table, the thread and the local store and nothing else. When the LLVM interpreter failed to build, that check disabled a fallback which was in fact available and dispatch took the "Compilation failed" path instead. The set is now also cleared per emulation session. Its keys are local-store offsets, which every SPU thread, every image and every title in the process reuse, so a set that outlived the session let one title's compile failures route an unrelated title's code at the same offset to the interpreter. The call is guarded by ARCH_ARM64: the set and its accessors exist only on that backend, which is the one that can fail to compile a block. tests/test_spu_failed_blocks.cpp covers both hole shapes, the half-open boundaries, the merge cases in both orders and the local-store extremes. Its load-bearing case is MatchesReferenceCoverage, a randomized differential against an independent bitmap, which constrains the union, the maximality of range_of and the "coverage grew" return value together for sequences nobody chose by hand. is_disjoint() has no reachable negative through the public API and is documented as a witness rather than presented as a check. The file also names the runtime paths it cannot reach. It is registered in rpcs3_test.vcxproj as well as the CMake list; the Windows CI job runs the MSVC build, where it would otherwise have been absent while reporting green. Executed. The ARM64 core builds clean, no warnings. The interval set passes a randomized differential run directly on the header (200 trials, 4800 mark operations, 0 mismatches); the pre-merge algorithm fails the same oracle 1268 times. On device (Snapdragon 8 Elite-class, Android 15), a throwaway build that forces compile failures drove the fallback end to end for the first time: a block with no prior mark recorded its whole 680-byte analysed extent in one mark, and a pre-marked block returned its covering range; both were interpreted inside the live gateway frame and escaped, with Mirror's Edge holding its title screen at 30.00 fps and Metal Gear Rising at 457 present frames over 8m44s with no "Compilation failed". Not executed. No x86-64 build and no rpcs3_test binary: the header's evidence comes from a standalone host harness and mutation runs, not from the registered gtest, and the ARCH_ARM64 guard on the session reset is unverified by compilation. The dispatch re-entry fast path recorded zero hits in every device leg, so the exposure from merging ranges -- previously-JIT'd addresses routed to the interpreter for the rest of the session -- is unmeasured. The same forced failure applied to the pre-change code did not fail on device either, so these runs show the new path is correct and free, not that it is necessary; the escape from a dead gateway frame needs a HALT, an MFC interrupt or cpu_work to fire while inside the interpreter, which one short interpreted block did not reach. |
||
|
|
6e731093c4 |
RSX: park the dma_manager::sync() wait instead of spinning
The RSX-thread branch of dma_manager::sync() busy-waited on the offloader with a pure pause() loop. Measured on Metal Gear Rising gameplay (Odin, warm shader cache, off-CPU profile): the loop held 26.6% of the RSX thread's wall time while the RSX Offloader thread itself was parked in a kernel wait for 99.78% of the same window -- the spin was paying the offloader's wake-up latency on every small handoff, burning about a quarter of a core to wait for a mostly-idle thread. Spin briefly for the short common case, then wait on m_processed_count with a 100us timeout. The offloader notify_all()s that atomic when its queue drains; the timeout is load-bearing, not a formality -- an offloader stopped mid-job by a memory fault cannot notify (it spins in on_access_violation until this thread's upkeep clears the deadlock flag), and the upkeep can itself enqueue new jobs from inside the wait, deferring the equal-counters notify to the next drain. on_semaphore_acquire_wait() still runs every iteration. Three refinements from an eight-pass adversarial review of the first version of this change: - The wait targets the processed count the loop condition observed, and parks only if a re-read after the upkeep call shows no progress. The drain-notify is one-shot: parking on a pre-upkeep value absorbs a full timeout when the offloader drained during the upkeep, and parking on a blind re-read turns any partial progress into an immediate return, degrading the park into a hot upkeep loop for the whole drain. - If the offloader thread is not running (config toggled on mid-session after booting with it off, aborting, or dead from an unrecoverable fault), the drain can never come; keep the visible spin there so the pre-existing hang stays attributable in a profiler instead of presenting as an idle, healthy-looking app. - The comment states the timeout's real role; the first version claimed nothing else could enqueue during the wait, which is false (the upkeep's flush path reaches backend_ctrl) and would have licensed removing the timeout. Measured after (same scene and script, healthy device): sync() falls to 0.10% of the RSX thread's wall time, the thread parks in the kernel for 67.6% of the workload, and fps is unchanged within run noise (52.9 avg vs 51.4 for the pre-review variant in the same session). The win is a freed core and its thermal budget, not frame rate. The non-RSX-thread branch has the same spin shape; it was not measured and is left untouched. |
||
|
|
e480c291da |
Emu: say more when a thread dies, and less when a game polls
The one-shot PPU state dump now follows its summary with what each PPU can report about itself -- registers, the guest call stack, and the recent guest and HLE/LV2 calls when PPU Calling History is on. Diagnosing the Saint Seiya stall meant reconstructing that by hand from a log that only named the thread; cia under the recompiler is written at block boundaries, so it names where a thread has BEEN, not where it is, and the call history is only populated by the interpreter. cellSysutil's parameter query drops from warning to trace. Eternal Sonata (BLJS10017) asks for ID_ENTER_BUTTON_ASSIGN twice every 33 ms and never stops, which is about sixty lines a second for an entire session. Games polling this is normal behaviour, not something to warn about, and the log volume alone is enough to slow the emulator down. |
||
|
|
7d25a7086e |
VK: frame generation through Lossless Scaling, experimental
Interpolates frames between the ones the game draws, at x2/x3/x4. The shaders
come from the user's own Lossless.dll; nothing is bundled or downloaded.
framegen runs on its OWN VkDevice and statically links volk, which defines 655
globals named vkCreateImage, vkQueueSubmit and so on -- including all 124 our
loader declares. Linked into the core those either fail to link or, worse,
merge, and framegen's volkLoadDevice() then repoints the whole RSX renderer at
framegen's device. So it lives in libarmsx3_lsfg.so, reached only by dlopen
with RTLD_LOCAL, behind a C ABI and a version script that exports eleven
symbols and nothing else. Verify with llvm-nm --dynamic --defined-only: only
armsx3_lsfg_* may appear.
Two devices with no shared semaphore means images cross as AHardwareBuffer --
Adreno and Mali both refuse vkGetMemoryFdKHR(OPAQUE_FD) on AHB-imported memory,
so upstream's FD path does not work on this hardware. Capture costs 0.007
ms/frame CPU, measured; the cost is the synchronisation, not the copies.
Notes for anyone reading this later:
* The shader loader's user pointer must outlive initialize(). framegen copies
the callback into ShaderPool::source and resolves shaders lazily while
BUILDING THE CONTEXT, so a stack local there is read back from a dead frame
-- a segfault executing at a mapped, non-executable address.
* The "device UUID" is not one. framegen matches (vendorID << 32) | deviceID.
Zero matches nothing.
* Imported shaders are cached to disk. They used to live only in the library's
map, so every restart silently had none and generate() returned 0 before
doing any work.
* Capture takes the COMPOSITED swapchain image, after overlays. Capturing the
game image put the perf overlay on real frames only, so it blinked at half
the display rate.
* generate() runs only on a frame the game actually drew, or the PPU/SPU
compilation screen gets interpolated too.
The pipelined path that would take waitIdle off the critical path is present but
disabled behind k_framegen_pipelining_enabled: holding a frame back conflicts
with frame-context recycling, and at least one reclaim path has not been found.
The serialised path is what works. Frame generation costs some real framerate
and wants a steady one -- interpolating an unstable rate reads as judder -- so
it is labelled experimental in the UI.
|
||
|
|
dbbb6fbde0 |
VK: use extended dynamic state to collapse pipeline permutations
Cull mode, front face, depth test/write/compare and primitive topology move out of pipeline identity and into per-draw state where VK_EXT_extended_dynamic_state is available. Fewer pipeline objects to compile and cache is worth a lot on Adreno and Mali, where first-run compilation is a visible source of stutter. Topology only collapses within its class -- triangle list/strip/fan share one pipeline, lines share one, points stand alone. vkCmdSetPrimitiveTopology cannot cross classes without dynamicPrimitiveTopologyUnrestricted, which comes from extended_dynamic_state3 and is not something mobile drivers report. The class representative is restart-aware: primitive restart on a *_LIST topology is illegal without primitiveTopologyListRestart, so a restarting draw is represented by the strip form or pipelines that build today start failing validation. Gated on the feature bit, not the extension string, and enabled at device creation; without it the props keep their real values and the command stream is byte-identical to before. Entry points go through the existing VKProcTable wrangler, so vk_android_loader needs no regeneration. pipeline_props keeps its shape: the disk cache stores it as a raw struct, so the VALUES are normalized before it is used as a key rather than teaching operator== about the extension. The shader cache directory becomes v1.96-eds against v1.96 -- the suffix matters because support depends on the DEVICE, and a driver can be swapped in through adrenotools between two runs of the same game. Reading a normalized entry back without the extension would silently build pipelines with culling off and depth compare NEVER. Depth bounds, stencil, and the EDS2/EDS3 states stay static: depth bounds is constant per device and never differentiated anything, and stencil is already all-zero for the overwhelming majority of draws. |
||
|
|
d069a55acc |
VK: a lost surface is recoverable, not fatal
Leaving the app during a game aborted the process outright: Assertion Failed! Vulkan API call failed with unrecoverable error: Surface lost (VK_ERROR_SURFACE_LOST) swapchain.cpp, swapchain_WSI::init() Losing the surface is routine on Android -- the ANativeWindow is destroyed every time the app leaves the foreground -- and the renderer already treats it as recoverable everywhere else, setting m_surface_lost in both the acquire and the present paths. Only swapchain init went through die_with_error. All three surface queries in init() now return false instead of aborting, and record which kind of failure it was. The caller needs that distinction: "the window is minimized, retry later" and "the VkSurfaceKHR is dead" both surface as a false return, but retrying against a dead surface queries the same dead handle forever. Only the second recreates the surface first. That also removes the memory corruption behind it. The fatal error killed the RSX thread mid-operation and the Main Callbacks thread then destroyed its objects, so tearing down ZCULL state freed a container that was still being written -- scudo reportInvalidChunkState inside ~ZCULL_control. No fatal teardown, no corrupted teardown. ~ZCULL_control is tightened regardless: it now drains page refs and resets prot the way unlock_pages does, rather than freeing pages that still hold references and leaving m_critical_reports_in_flight unbalanced -- harmless at process exit, wrong on a restart within the same process, which is every restart here. Note its m_pages_mutex is the only place that lock is taken; every real writer is externally synchronized and locks nothing, so holding it must not be mistaken for protection against a writer that is still running. |
||
|
|
614bf8b718 |
Android: re-deliver the Surface, so a missed one cannot strand the renderer
Opening a game the instant the app started left a black game area forever, while rotating the device "fixed" it. SurfaceHolder.Callback::surfaceChanged is a one-shot -- Android delivers it when the surface is created or resized and never repeats -- and getNativeWindow() blocks until that single delivery arrives, in a 100 ms sleep loop with no timeout. One missed delivery therefore parks the RSX thread for the rest of the session. A rotation only helped because a configuration change forces a fresh surfaceChanged. EmulationSurface now re-delivers holder.surface on attach and on window visibility changes. It is idempotent: the native side compares the incoming ANativeWindow against the one it holds and no-ops on a match, so this costs nothing when the first delivery already arrived. It has to be post()ed, since onAttachedToWindow runs before layout and a 0x0 report is explicitly ignored. The wait loop also logs now, every three seconds, because the failure was otherwise completely silent: the emulator log stopped dead just after Vulkan device creation, the perf sensor read 0.0% CPU, and nothing said why. Diagnosis took a screenshot and dumpsys SurfaceFlinger to establish the surface existed. Adds GSFrameBase::display_epoch, bumped when the native window is replaced. The swapchain is rebuilt on a size mismatch and nothing else, so a replacement window at identical dimensions was invisible; platforms that cannot swap a window under a live swapchain keep the default and are unaffected. |
||
|
|
cce09dbb39 |
SPU: recover from a failed analysis, and stop the log floods
Three faults that showed up in tester logs, all of which made the emulator look broken in ways the log then hid. Eternal Sonata flooded with SPU "Invalid code" errors: when the analyser produced no data the recompiler had an empty branch with a TODO where the fallback belonged, so the block was neither compiled nor marked, and the same address was retried forever. It now marks the block failed and lets the interpreter take it -- 6320 errors in one session down to none. The unknown-instruction and halt messages are rate-limited, per opcode and per address rather than globally, so a repeating fault reports once instead of every execution. One tester's log went from 600 MB to 2.0 MB; the log volume itself had been slowing the emulator, so this is not only a readability fix. ARM64 fault classification in Thread.cpp preferred a heuristic comparing si_addr against the PC, which misreads a genuine data fault as an instruction fetch. It now decodes ESR first and only falls back to the heuristic, and an SPU halt at the 0xffdead00 sentinel is reported as a guest assertion rather than a host segfault. BLEACH crashed here, and the misclassification gated every recovery path behind it. |
||
|
|
b5a715adcf |
PPU: give the AArch64 register scavenger the spill slot it needs
Saint Seiya: Sanctuary Battle (BLES01421) stalled partway through PPU compilation and booted to a black screen. The failure was in LLVM, not here: on AArch64 the register scavenger ran out of registers under the GHC calling convention, which pins most of the GPRs to guest state, and AArch64FrameLowering::determineCalleeSaves returns early for GHC before it can create the emergency spill slot the scavenger falls back on. The scavenger then aborts, and because that takes down the whole MODULE rather than one function, every function in it drops to the interpreter -- the boot never finishes, or the game runs at interpreter speed with nothing in the log to explain it. Fix creates the spill slot for GHC frames that actually need stack. 231/231 modules compile for Saint Seiya, and Sonic Unleashed's FMVs work for the same reason. Because it is a codegen fix rather than a per-game workaround, any title that hit this benefits. The change itself lives in the LLVM submodule, whose remote is upstream llvm/llvm-project, so it cannot travel in this repository. It is preserved here as 3rdparty/llvm/armsx3-aarch64-ghc-emergency-spill.patch, applied against the pinned submodule commit; a build without it applied will exhibit the original stall. Also bumps the ARM64 codegen cache version so caches produced before the fix are not reused, and carries the PPUTranslator changes the same work needed. |
||
|
|
eb54f9b75a |
Build: four release variants, and what the new one needed
Splits the Android release into legacy / a11 / a13 / a15 so a device can take a build matched to its CPU and OS instead of one binary suiting everything. android/build-variants.sh drives all four from a single table of (ndk, api, -march, apk suffix), and ConfigureCompiler.cmake takes -march per variant rather than hardcoding one. The legacy variant had never been compiled before: every release up to 0.7.2 was built at the gradle default of minSdk 33, so nothing had ever targeted a lower API. Doing so turned up std::aligned_alloc, which is API 28+ -- below that <cstdlib> does not declare it at all and the using-declaration fails to resolve. posix_memalign is the older spelling and its result frees with plain free(), so the rest of the header is unaffected. Kept even though legacy now targets API 30, because it costs nothing and the next person to try a lower floor should not rediscover it. legacy targets armv8.1-a, which is the floor this codebase compiles at rather than a preference: util/simd.hpp uses SQRDMLAH (v8.1 RDMA) and util/asm.hpp has inline LSE atomics, so armv8-a does not build. Its value is cores that are ARMv8.2 without the OPTIONAL fp16 and dotprod extensions the other three variants require. Cortex-A53/A72/A73 class parts stay out of reach until those two paths gain fallbacks. |
||
|
|
0a9fd15b57 | Merge branch 'pr41' |