mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
memcard-rollback
24982
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
112bc73c4c |
Android: take a card snapshot at launch, and offer the restore
Wires MemoryCardBackup into the app. The snapshot is taken immediately before the emulation thread starts, in MainActivityRuntime.start() and startBios(). At that instant the card file is not open, so the copy cannot catch a half-finished write and there is no thread timing to reason about. It also means the copy holds the card as it stood when the player last finished successfully -- if this session is the one that breaks things, the snapshot is clean by construction. Restoring then loses the current session's saves, which is the trade a save-state slot already makes. The BIOS boot gets one too: its memory card manager can format a card or delete saves off it, so that session is worth a copy for the same reason a game is. Launch also checks the cards it is about to mount. If one will not read AND a verified backup exists, the boot is HELD and the prompt offers to put it back before the game starts. That ordering is not cosmetic: once the console has mounted a card it caches its own picture of the directory in guest memory, and a restore underneath would be written straight over. "Start anyway" stays available -- some people will want to format fresh -- and is remembered only for the launch it was answered for. The memory card screen gets a per-card Backups panel: the snapshots with their date, size and the game that was running, a verified-or-suspect badge, restore, back up now, and the automatic-backups switch. That manual path is the one that actually matters, because the automatic offer cannot fire for the failure players hit most -- a card that verifies perfectly while the save inside it is damaged. Recognising that would mean understanding each game's save format. Restore is refused while a game is running, for the cache reason above, and says so rather than failing quietly. A suspect snapshot is listed rather than hidden: the pre-restore copy of a broken card is exactly what someone may need back. |
||
|
|
8e07613231 |
Android: rolling per-card memory card snapshots
Nothing today keeps a previous version of a memory card. A file card is written
straight through to disk as the game plays, so the card on storage is always the
only card there is; anything that interrupts a write leaves the real card
damaged, and the player finds out when they load.
MemoryCardBackup keeps a small rotation of snapshots per card so that is
recoverable. This commit is the engine only -- nothing calls it yet.
Two rules do the real work:
- A card that fails verify() is never snapshotted. There is nothing worth
saving and a rotation slot to lose. That same check is what detects the
user's problem, so it is reported rather than swallowed.
- prune() never deletes the newest snapshot that passed verification. Without
that, the rotation is a shredder: the card breaks, the player relaunches five
times trying to work out why, and every good copy has been overwritten with a
copy of the broken card.
Retention is the newest 3, plus the newest from each of the preceding 4 distinct
days, plus always the newest verified copy however old. Three copies from one
afternoon only protect against that afternoon.
verify() is the same test the core trusts (FileMcd_IsMemoryCardFormatted): the
PS2 format signature at the head of the card, or the folder card's superblock
marker. Deliberately a size FLOOR rather than a table of exact sizes -- strict
about the signature, permissive about the size, so it refuses damaged cards
without refusing unusual ones that work.
Snapshots are content-hashed, so relaunching without saving costs nothing. They
are written to a .part file and renamed into place, so a process killed mid-write
leaves no truncated archive that would list as valid -- the same class of bug
this feature exists to undo. Restore stages and swaps through a sidelined copy,
outside the cards folder: a folder card is a directory, and the card list shows
every directory under memcards/ as a card, so staging there would flash a phantom
card mid-restore and leave one for good if the process died.
Restore takes a snapshot of the live card first, unconditionally, including a
broken one marked as such. Restoring the wrong copy must not be the act that
destroys the evidence.
Written against the Java file APIs rather than the core on purpose. The storage
memory cards live on is FUSE-emulated on some Android configurations, where
libc's file-creation call is denied outright -- a problem this tree has hit twice
and worked around both times from Java. A snapshot writer in the core would fail
silently on exactly the devices that need it.
The rotation joins the whole-app export: a card restored onto a new phone from
that archive is just as likely to be the broken one, so the history that can undo
it has to travel with it.
|
||
|
|
cd20f6454d |
Android: flush memory card writes when the app is backgrounded
A memory card write does not necessarily reach the file system when it happens,
and on Android that is data loss rather than a detail.
A FOLDER card holds writes in an in-memory page cache and flushes two frames
after the last one, counted down by the per-frame tick that runs off vsync. So
pausing does not delay that flush, it stops it ever being reached -- the counter
does not advance at all while the VM is paused.
A FILE card writes through stdio with no flush anywhere in the path. Seeking on
an update stream pushes the previous write out, so a run of writes mostly
self-corrects, but the last write of a save sequence sits in the buffer until the
next card access or fclose.
Either way the pending write is lost if Android reclaims the process while it is
backgrounded, which it may do with no further callback. Save in-game, switch
apps, get reclaimed, and the save was never on disk.
The pause path already handles exactly this shape for the BIOS NVRAM
(cdvdSaveNVRAM, added because "the process is frequently killed while paused"),
so the card flush goes next to it, in both the Running and already-Paused
branches. It runs on the CPU thread, queued after SetPaused, so the console is
stopped and nothing can be written behind it, and it is fire-and-forget -- onPause
is on a deadline and blocking it risks an ANR.
- FileMcd_Flush() / FileMemoryCard::Flush() / FolderMemoryCardAggregator::Flush()
write out what is buffered without closing anything, so the console keeps
playing afterwards.
- FileMemoryCard::Flush deliberately does NOT stamp the running checksum the way
Close() does. That value is a change-detector a savestate load compares to
decide whether the card moved under the console, not an integrity check, and
m_chkaddr is card data rather than a header field we own. A stale value costs
one auto-eject on the next savestate load, which is the safe direction, so
writing to the card on a path upstream never writes on buys nothing.
- FolderMemoryCard::FlushNow clears the frame countdown so a resumed VM does not
repeat the work. Flush() is already a no-op when nothing is cached, so calling
this on a quiet card costs nothing.
- Save() flushes each sector as it is written.
Also bounds the emulation-thread join in onDestroy. NativeApp.shutdown() already
gives up waiting after 5 s and returns anyway, so an unbounded join inherited a
wedged CPU thread and hung the destroy path until Android force-closed us.
|
||
|
|
df17121cc1 |
SIO/Memcard: refuse a card write whose read-back failed
FileMemoryCard::Save is a read-modify-write: it reads the sector being written into a scratch buffer, ANDs the new data into it (memory card bits only ever go 1->0 without an erase), and writes the result back. When the read failed it reported the error and then carried straight on into the merge. m_currentdata is only ever grown, never cleared, so the merge ran against whatever an earlier -- and possibly completely unrelated -- write had left in the buffer, and that was what got written to the card. A transient read failure therefore did not lose a write, it corrupted a sector the console never asked to change. Refuse the write instead. The sector keeps its previous contents, which the console can retry. The return value is discarded by the only caller (Sio.h), so this reads as "don't write" rather than as an error report -- the same shape as the two Seek failures already in this function. Desktop hosts rarely see a read fail on a card file. Android does: the storage memory cards live on is FUSE-emulated, where this tree already documents libc calls being denied outright. |
||
|
|
dfef534426 |
UI: the Android internal-resolution set in the Qt and Big Picture pickers
Both desktop resolution pickers ran Native, 2x, 3x ... in whole steps. That is the wrong shape for a handheld at both ends: below native is unreachable even though rendering there is a large win and the GS accepts it -- only the top end is clamped, in GSClampUpscaleMultiplier -- and above native the jump from 1x to 2x is four times the pixels with nothing in between. Both pickers now carry the set the Android UI has offered since issue #207: 0.25x 0.5x 0.75x Native 1.25x 1.5x 1.75x 2x 2.25x 2.5x 2.75x 3x 3.5x 4x 5x 6x 7x 8x Everything above 8x moves behind Extended Upscaling Multipliers, which used to unlock 13x and up. That checkbox was dead on ordinary hardware: the GPU cap is max texture size / 1280, so a 16K-texture part reports exactly 12x and the "supports extended" test wanted more than 12x. It now unlocks anything past 8x, which is the first time it does something on a normal GPU, and 9x-25x still appear only as far as the GPU can actually go. Big Picture indexed two parallel arrays as "slot i means multiplier i+1", which a non-uniform list breaks. Replaced with one table carrying the label, the INI string and the multiplier, filtered against the cap. The quarter steps are all exact in binary, so matching a saved multiplier by equality is safe. Two things fixed in passing, both of which the new list would otherwise have broken. The Qt global tab keyed "nothing found" off index 0 being Native, and index 0 is now 0.25x. And Big Picture passed "1.000000" as its default while the INI is written by StringUtil::ToChars, which produces "1" -- so with no key set the picker read back a value matching no entry and displayed Unknown.nightly-20260820 |
||
|
|
7f0ae7a6c6 |
GameDB: internal FPS by DISPFB blit for NASCAR Thunder 2002 through NASCAR 06
The register-write detector reads these engines as producing a new image every vblank, so duplicate-frame skipping never fires and every repeat pays for a full present. Counting blits into the displayed framebuffer restores it. Reported-by: yuasasanightly-20260819 |
||
|
|
be72a8e1eb |
iOS: put Download Shaders under Preset, where it can be reached
It shipped as its own Section on the settings page, which put it below the parameter list. With crt-aperture selected that is twenty-two sliders and about thirty-nine swipes, on the one control a tester had asked for by name. Found by walking the screen in the simulator rather than by reading it, which is the only way this kind of thing turns up. It moves into ShaderChainSection, directly under Preset and above Install Shader Pack, so the three ways to get a preset sit together in the order you would try them: pick one you have, download one, install one from a file. That also puts it in the in-game pause panel, which is a gain rather than a side effect, and it works only because GameScreenView wraps the shared section in its own NavigationStack. Without that every NavigationLink in the section is dead on tap, Preset included, so the fence now checks for the stack in the text immediately around the mount. Immediately around, and not anywhere earlier in the file, because the first version of that check searched backwards from the mount through the whole of GameScreenView and any one of its several other NavigationStacks satisfied it. Deleting the one that matters left the suite green. That is the third time in this branch an assertion has been satisfied by a neighbour, and the only reason any of the three were caught is that each new check was run against a deliberately broken source before being trusted. The row sits outside the enabled gate, which the first attempt at this move got wrong. Every other row in that section is behind `if enabled`, and putting the download row there too hid it whenever the chain was off -- so a first run had nothing to select, no way to fetch anything, and no hint that the toggle came first. Caught by relaunching with the chain off and looking, one screenshot after the change built. |
||
|
|
4fa820435d |
iOS: decode a launch link's filename once, not twice
queryValue returns its answer already percent-decoded on both of its paths: URLComponents decodes for the ordinary case, and the raw-query fallback decodes by hand for callbacks that arrive unencoded. launchGame then decoded it again. A second pass reads a literal percent in the value as the start of a new escape. 100%.iso percent-encodes to 100%25.iso, the first decode gives back 100%.iso, and the second sees % followed by .i, which is not hex, so removingPercentEncoding returns nil. The guard falls through and tells the player the link is missing a game filename, which is the one thing it plainly carries. Only the launch route did this; exportLibrary reads the same helper and does not. The contract now sits on queryValue rather than being something each caller has to know, since knowing it is what went wrong. The fence pins more than the fix, because armsx2://launch?game= is not an internal detail. libraryPayload hands that string to other frontends, which store it and replay it much later, so the verb, the parameter name and the encoding are a contract with software this repository does not control. It also compares the schemes the handler accepts against the ones Info.plist registers, in both directions: a scheme in code but not in the plist fails silently, because iOS never routes the URL and the handler that would have accepted it is never reached. Six mutations run against the real source, all six caught, each restored byte for byte. The scheme check needed the second direction to catch the sixth; the first version iterated the known list and could not see an addition. |
||
|
|
05d94ed82d |
iOS: download the RetroArch shader collection from inside the app
867 presets over 27 categories, one manifest and one zip each, and no third request anywhere. A tester asked for what Manic EMU has: a button that fetches the collection instead of making people find a zip and side-load it. The closure resolution and the licence sign-off landed first; this is the phone half. Order is the whole safety argument, because a remote manifest is attacker- controlled if the host is. The stated size is refused before the transfer rather than after -- the manifest carries it, so the refusal costs nothing. The received byte count and the SHA-256 are both compared before the importer is called, and the hash is streamed rather than read whole. The relative path is validated before it becomes a URL, because .. and / both survive percent-encoding. Then the fenced extractor does the writing, unchanged, so there is no second containment guard to get wrong. The manifest is 8 MB raw and 312 KB gzipped, and 96% of those bytes are the per-file array. The entry type does not declare that key, so it is skipped: the zip carries its own hash and that covers every file inside it. What lands in the cache is this build's own projection rather than the served bytes, which is also what makes browsing work with no network -- a failed refresh ages the list instead of emptying it. Two things the import path never had. Staging files are swept at launch, because defer does not run when iOS kills a backgrounded app mid-download, which is the ordinary outcome and not an edge case. And cancelling removes the pack if the extract already began, which is not the same as stopping it; the comment says so rather than implying otherwise. The importer returns the name it installed instead of only publishing it. One property on a shared object is fine for one caller and wrong for a screen with 867 rows and no reason to install them one at a time: two installs overwrite each other's answer, which would write one entry's marker into the other's folder and make cancelling one delete the other. Three fixes in the code around it, from the same review. A loaded chain owned a render target and a pipeline per pass and nothing freed any of it when the player turned shaders off, because DestroyShaderChain had exactly two callers, a preset change and device teardown. The Metal frame path flushed on success and returned on failure, though a chain that failed partway has already encoded passes into the same command buffer and needs the submit for the same reason the success path does. And the pack extractor held every file's bytes resident to the 32 MB cap, because the autoreleased data was never drained inside the loop. The catalogue is not published yet. The base URL is one constant, and an INI key no UI writes can repoint it, accepting only https and file -- which is how a simulator reads a local emit, since ATS refuses plain HTTP and there is no reason to weaken it for a test. Eleven checks in the new fence, six mutations run against the real source and all six caught, each restored byte for byte. Still open and written down rather than left to be rediscovered: the extractor's per-entry decompressed cap is applied after the entry is fully inflated, so a crafted zip can spend up to that cap before the refusal. Bounding it earlier needs a streaming inflate. The extractor's own fence gains an ordering claim. It asserted that a canonical resolve appears somewhere in the method, which passes for a resolve whose answer is discarded; deleting the entire containment refusal left it green. It anchors on resolvedParent now, because the body carries several refusal sites naming the same constants and anything looser is satisfied by a neighbouring refusal that has nothing to do with containment -- which is the same trap the first attempt at this fix fell into. |
||
|
|
af30d18304 |
iOS: three defects the branch review found, two of them silent
Seven lenses over the branch, each finding then handed to a skeptic told to refute it rather than confirm it. Thirty-one were raised. These three survived and matter, and two of them fail without saying anything, which is why none of them turned up in a device pass over a green suite. Per-game write() set the enabled key straight from the picker, before the guard that needs the preset to resolve. Choose On, delete the pack the preset came from, then save any unrelated row on that game: the file keeps the chain enabled and loses both preset keys, and an absent key in the game layer falls through to the base layer. That game then renders the GLOBAL preset. The type's own first comment says this never happens and boot-time repair has always got it right; write did not. It is the worst kind of wrong because it is invisible -- one CRT shader looks like another, so the player sees a filter and assumes it is theirs. A per-game preset never received its saved parameter values at all. SettingsStore pushes the global tier's overrides at launch and on every change, but a per-game preset is chosen in a file SettingsStore never reads, so the game rendered the shader author's defaults and every value saved against that preset was ignored. The boot repair already resolves that token before bootISO, which is the one place that knows both the token and the timing; pushStored is nonisolated now so it can be called from there without hopping actors and losing the ordering. Save as New Preset could destroy the preset it was saving from. The reference it writes is relative to My Presets and the sheet pre-fills the base's own name, so selecting a saved preset, nudging a value and accepting the default replaced that file with one whose only reference is its own filename. Nothing resolves that, and the values it held are gone. It refuses now, in the write path, which is the only place that can see both the target and the base. |
||
|
|
30d9816eda |
iOS: resolve the shader catalogue off-device, and sign what it may ship
The half of the downloader that cannot run on a phone: a generator that turns any preset in a pinned slang-shaders tree into a complete, path-safe, licence-classified file closure, and refuses to emit anything until a person has signed the rules it would be built from. Resolving a closure means walking includes and references across a 5,000-file tree. Over the GitHub API that costs two to five requests per preset against a 60-per-hour limit keyed to the originating IP rather than to the app, so every user behind one carrier NAT shares one budget. On a local clone it costs fifteen seconds of CPU and no network at all. That asymmetry is the whole design. emit refuses without a signed rules file recording the pin, so the catalogue cannot physically exist before the nine class questions were answered. Six were confirmations of rules the bundled sixteen already ran under. Three had never been decided and were worth 577 presets between them, and the one that mattered was whether a LICENSE file governs the directory it sits in -- worth 552 on its own, and exactly the inference the standing rule exists to refuse. Admitted, with the reasoning in the signed document rather than here. Of 2,553 presets in the tree, 867 are offered: 13 dropped on upstream defects, 8 on an extension the extractor will not write, and 1,665 excluded by class. Fourteen presets the earlier hand audit had measured agree row for row on file count and on upstream bytes, which is the free correctness check on all of it. The whole-tree run found the divide-by-zero prescale in ten more files than the two bundled ones, refusing 98 presets. Twelve sites and not ten, because the scanner reports one per file and clamping the first in crt-potato and ultra_potato made a second visible in each; the scan was re-run until it came back empty. All twelve now carry a notice in the file itself saying it changed and when. ATTRIBUTION.md covers the bundle and covers nothing once the same file travels in a zip on its own, which is where GPL section 2(a) asks for the notice anyway. The first wording of that notice said the change was "one max() and nothing else", and the guard test looked for max() anywhere in the file -- so the comment describing the fix satisfied the test that checks the fix exists. Both were changed: the notice says clamp, and the test now requires the guard on a line that actually matches the prescale pattern. |
||
|
|
e2b1bcaea2 |
iOS: let one game keep its own shader preset
A preset was a single global value, so picking crt-geom for a 2D fighter also applied it to the next 3D game booted. The per-game subsystem is the right home; the blocker was that its bridge exposed Int, Bool and Float and no String, while a preset is a string token. Four String accessors added, in the forISO and the current-game shapes the twenty existing per-game settings already use. A Shaders section on the per- game Graphics tab, where the global Shader Chain section sits, on the same tri-state sentinel every other control there uses: use global, off, on. Scope is preset only, decided with both arms in front of the developer. Parameter values stay global and stay keyed by preset token, and the panel says so on screen rather than leaving it to be discovered. A preset exposes up to twenty-two values and per-game copies of those would multiply the storage and the UI. The identity is the same root token the global tier uses, re-rooted at boot before bootISO reads the file, so a per-game choice survives a reinstall for the same reason a global one does. ShaderPresetLibrary.resolve stays the only token-to-path resolver; nothing here reimplements containment. The rule that matters, and the one the fence exists for: a token that no longer names a file turns the chain off for that game rather than falling through to the global preset. A different CRT shader looks like a CRT shader, so a substitution is invisible -- the player sees a filter, assumes it is theirs, and never learns their choice is gone. Six source checks over the six files the selection lives in, and four mutations run against the real source with every restore byte identical. Also here, because it landed in the same wave: the prescale fence widens to .inc and .h. A .slangp names its stages, but a stage includes whatever it likes, so that bug can sit in a header and never appear in a .slang -- which is exactly where the whole-tree catalogue run found it. |
||
|
|
7e8f7f1955 |
iOS: give the shader controls their own page and a pause-menu route
Four things a tester asked for after playing the first build, and the two defects found while building them. Shaders are their own settings page rather than a section inside Graphics. The section already took its persistence from the caller, so this is a move and a root row. The same controls reach the in-game Quick Menu, under Game Tools rather than Quick Actions. Not a drop-in: the settings section is Section-shaped and embeds a push, while the Quick Menu is card-shaped with no navigation stack, so it routes out to a sheet the way the speed panel does. A test holds it to that shape, because the shape is the thing that works rather than an implementation detail. Parameter rows use NumberRow, the control eleven other settings files already use, so a value can be typed instead of only dragged. Detents, units and the reset affordance come with it. Every label is translated into the nine languages beside English. Then the two defects. A preset's saved values reached the core only when a shader screen was open, so a cold launch rendered the author's defaults until the player visited the page -- proven by measuring frame luminance across a launch rather than by reading the code. And the guard that keeps SettingsStore.init off SettingsStore.shared was recovered from an orphaned commit and turned out to be broken: a plus-or-minus 400 character window let an allowlist entry cover its neighbour, so the test would not have caught the crash it was written for. It requires the match to span the access now. |
||
|
|
d24e9d76ed |
iOS: clamp a shader's prescale so upscaling cannot blacken the frame
crt-aperture and sharp-bilinear each derive a whole-number prescale from output height over source height and then divide by it. RetroArch only ever feeds them a small console framebuffer being scaled up, so that ratio never falls below one. PCSX2 renders internally at up to 8x: past roughly 1.5x on a phone the source is taller than the screen, the ratio drops under one, floor() returns zero and the divide yields NaN. The whole frame goes black. Reported on an iPhone SE 2 with a 1334x750 window, where 1.5x rendered and 2x did not. Two hypotheses were wrong first -- push-constant placement, then parameter placement -- and both were refuted by tester data before the reporter supplied the actual trigger, which was the internal resolution and not the preset. Reproduced in the simulator at 3x and fixed there. The clamp is what the sibling sharp-bilinear-simple already carries as max(floor(...), vec2(1.0)) and what crt-geom carries as clamp(floor(...), 1.0, 2.0). Nine of the eleven bundled presets never divide by a derived scale and were unaffected. These files are otherwise byte-verbatim copies of a pinned upstream commit, so the divergence is a reversible patch beside the librashader one and a note in ATTRIBUTION.md. The test fails if either guard is dropped, which is what a re-sync from upstream would otherwise do silently. |
||
|
|
16b571cf72 |
iOS: put the shader chain and its parameters in settings
A section in Graphics after Shade Boost, matching pipeline order, a folder- at-a-time preset browser, and every parameter a preset declares on screen. The section is absent rather than disabled in a build without librashader, gated on a bridge capability, so a cargo-less build does not advertise a feature it cannot run. Every number in a preset's parameter block is the shader author's, so every number is treated as hostile. Absent, non-finite, inverted ranges and a zero step all occur in the published collection. A parameter whose range cannot be made sense of is dropped rather than rendered as a control that does nothing. Pushing a value sends the effective value of every parameter, not only the changed one. librashader has no unset call, so a name dropped from the override map would leave the chain on whatever was pushed last and a reset would never take. A tweaked preset can be saved as its own file: a #reference to the base plus the changed values, written into My Presets inside the scanned root so it becomes selectable with no extra plumbing. The reference is relative while the base is in Documents, so the pair survives the container moving; a bundled base gets a path instead, which a reinstall breaks, and the sheet says so. The naming sheet is .sheet(item:) rather than .sheet(isPresented:), because the parent's body invalidating tears the content down and takes keyboard focus with it, which is the failure this codebase has a rule about. Also here: the once-cached name lists are owned rather than read after free, and librashader builds for the simulator as well as the device, which is what makes any of this testable without hardware. |
||
|
|
f23ddabf1e |
iOS: run RetroArch shader chains on the Metal renderer
librashader built for arm64 and pinned, wired into GSDeviceMTL, and a preset library behind it that can name a file the same way twice across a reinstall. The chain runs from DoApplyShaderChain, after ShadeBoost and before present, on the same ping-pong the FXAA path uses. Two things about it are load- bearing rather than incidental. EndRenderPass comes first, because the chain opens its own passes and Metal aborts if ours is still encoding. And FlushEncoders comes last, because librashader recycles its per-frame objects over a ring shallower than our deferred-submit window, so a chain frame has to end the batch. A failure latches on the preset that caused it, or a preset that will not compile recompiles every frame forever. Static archive rather than dylib, decided by building both against a working tracer and measuring, and the loser was deleted rather than left as an option. The library underneath is where the reinstall problem lives. Both preset roots sit under a container UUID that changes on every sideload, so a selection stored as an absolute path is stale within days. A preset is stored as a marker plus a root-relative path -- bundle: or data: -- and re- rooted at launch. The separator is a colon because Files refuses one in a name and it is not a path separator, so the relative half never needs escaping. Packs come in as a picked zip or folder through an extractor that keeps the directory tree, because a .slangp names its stages by relative path and the tree is part of the pack rather than an arrangement of it. That is the opposite of the skin extractor's flattening policy, so a test fences the two apart. Sixteen presets ship in the app, each cleared against its own header rather than a blanket grant. librashader's own cache goes to Library/Caches through XDG_CACHE_HOME, set before anything loads it. Latent today because the Metal runtime never reaches that cache, but a pin bump that adds caching would otherwise put a disposable file somewhere iOS can neither purge nor keep out of a backup. |
||
|
|
5b616729f1 |
EE rec: stop raising TLB misses on unknown MMIO too
The _ext_mem* fallbacks raise a TLB exception when a registered region gets an access its device has no case for. Under a recompiler that is the defect just removed from vtlb_Miss by another route: nothing diverts the block, so the raise only latches Status.EXL. Raise on the interpreter alone. Recompilers report instead, which is new - MEM_LOG is devbuild-only, so the raise was all a release build left. |
||
|
|
040104142a |
Tests: pin the I immediate against a rewrite of micro memory
Both polarities of the gamefix. With it on, a rewritten immediate has to reach the block already compiled; with it off, the rewrite has to force a recompile. Each row asserts the compile count as well as the result, so neither can pass by recompiling behind the value it checks. Three things the harness needs handling for. The first re-entry through RunJitPreserveBlockCache compiles a second block variant, because it enters on the pipeline state the previous run left rather than the post-Reset one, so the block only goes warm on the re-entry after that. LoadProgram writes VU.Micro directly and so bypasses the vtlb path that calls mVUclear, which leaves the stale quick slot serving the old program unless the test runs the invalidation itself. And the pair that carries the immediate writes VI[REG_I] again on the way out, as does the delay slot LoadProgram appends, so the register reads back 0 and the assertion belongs on the VF result. |
||
|
|
19cb54586f |
microVU: read the I immediate at run time under IbitHack
x86's doIbit picks between folding the immediate into the block and loading it from micro memory; arm64 only ever folded. Scarface (SLUS-21111), the game the gamefix is listed for, writes a per-object transform into VU1 micro memory as I immediates. Under the recompiler its trees and much of the geometry around them took whichever object's transform compiled first. The interpreter re-reads the word on every dispatch and was unaffected. |
||
|
|
c5ad9ddd4d |
EE rec: stop raising TLB misses, matching x86
vtlb_Miss raised the exception under the arm64 recompiler and returned, on the assumption that the rec would pick cpuRegs.pc up at the next dispatch. Nothing picks it up: the block runs on and its tail writes its own branch target over the vector PC, while cpuException has already latched Status.EXL. cpuException leaves EPC alone whenever EXL is already set, so from the first swallowed miss onward every exception keeps its predecessor's EPC, and the guest kernel's syscall epilogue erets to an address belonging to the fault. Report the miss and continue instead, as the x86 rec already does. The guest's handler still does not run, so a title that needs demand paging still cannot work under the rec; a stray miss now stays local to the instruction. ee_rec_tlb_divert_tests.cpp describes the divert and stays disabled. With nothing left to set s_recTlbMissOccurred, the poll after every interpreter call goes too. It could not have covered the raise anyway: only the interpreter-call sites had it, so a flag set at an inline access sat there until an unrelated later recCall consumed it and diverted on a pc belonging to neither. This gives up the one case the rec did handle, a miss in a branch delay slot reaching the vector through the cpuRegs.branch bracket epilogue. Its recompiler half moves to the disabled file; the interpreter half stays live, beside a new test pinning the rec's behaviour. The bracket itself stays: the tlb_fallback_* handlers raise from a delay slot on their own. |
||
|
|
249aefdb9b |
Tests: pin the rec's TLB-miss divert, disabled
A TLB miss on an inline load or store leaves the arm64 EE rec at exception level in user code. Eight cases, all disabled, because the rec does not do this yet and the route to making it is staged: x86's behaviour first as a floor, then the divert built back up with these dropping their prefix one at a time. Found from the other end. `3D Pinball Space Cadet (PS2) (3.0) (RA)` hangs after "Parsing complete. Finalizing...", and the visible fault is a thread id of 0xff966c22 arriving at a caller whose syscall returned 1. That is strlen's `subu v0,v0,a0` on the correct v0, reached because the kernel's syscall epilogue eret'ed into the middle of strlen: 131072 of the run's next 162766 exceptions were taken with EXL already set, so none of them updated EPC. Upstream of all of it is one swallowed miss on a strlen(NULL). Two working hypotheses died on the way — that the pinned-GPR cache lost v0, and that the call-ret shadow stack mispopped — both refuted by reading the state at the moment of damage rather than by reasoning about the emitters. The tests are the chain in four instructions, plus the load and store halves of both inline emitter pairs, plus the flush the divert needs. Three separate defects fall out of the one missing poll. The block runs past the faulting load. EPC names the instruction after it, because cpuTlbMiss skips its `pc -= 4` for the rec while the rec's own cursor is already one instruction ahead outside a delay slot — the delay-slot case comes out right only because two errors cancel. And Cause is whatever exception came last, describing a different instruction than EPC does. Two findings shape the fix rather than the tests, so they are recorded here. Reaching the vector with guest state intact costs one writeback, not a general flush: iFlushCall(FLUSH_VTLB) already precedes every inline access and frees the caller-saved hosts, leaving only x28, the allocator's single callee-saved host. A boot with fastmem off puts a live dirty guest GPR there at 489 sites and nothing else anywhere. The fastmem backpatch thunk is the part with no clean answer. It is generated at fault time and cannot name the live guest values of the block around it, so it cannot divert. A census of the 12411 fastmem sites emitted during that boot says how much per-site state a precise one would need: 9355 have nothing live and dirty, 2480 have between one and seven GPRs, and 576 involve the NEON file. Lesson, from a guard test that was written wrong first. Three dirty registers before the faulting load is not enough to make the allocator reach x28, so that test passed with the writeback deleted — it guarded nothing. It takes sixteen live guest values at once. A test that guards a writeback has to create the pressure that puts something in the register the writeback exists for, and the way to find out is to delete the code and watch. |
||
|
|
83f2510134 |
Tests: pin the VU FMAC's range against the console
The VU's largest value is 0x7FFFFFFF, one binade above FLT_MAX, the same range the EE FPU has. So an exponent-255 word is an ordinary number on the way in and on the way out, and "overflow" starts above it rather than above FLT_MAX. Both engines put the boundary a binade lower. vuDouble() rewrites an exponent-255 operand as 0x7F7FFFFF and VU_MAC_UPDATE() calls every exponent-255 result an overflow; the arm64 COP2 macro emitters clamp the result to +/-FLT_MAX and raise neither O nor U. microVU's per-op operand clamps approximate the same thing from a list of games rather than a rule. 68 rows off an SCPH-90000 through VU0 macro mode, scored per engine and per column, with what each engine cannot yet reproduce recorded per case so a fix trips the test as loudly as a regression. Nothing is fixed here. Two of the rows are structural rather than about range. An overflowed product does not become 0x7FFFFFFF before the accumulate: an addend of -0x7FFFFFFF cannot cancel it. An underflowed product does become zero before it. And the multiplier is the EE's, with the same one-ULP deficit decided by ft's mantissa alone. The harness grows two things the rows need: a VADDA encoder, and an opt-out from Run()'s VU0 JIT-vs-interp auto-diff for tests that score each engine against a hardware capture instead of against the other engine. |
||
|
|
2a98726692 | Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7 2.6.6.7 nightly-20260817 | ||
|
|
5b790427dd |
LSFG/FSR: remove the debug instrumentation
The per-second LSFG branch counters and the FSR gate line were added to find two specific bugs and both did their job — the counters proved generated frames were reaching the screen uncounted (VK_SUBOPTIMAL_KHR treated as failure), and the gate proved all three FSR conditions passed while a misplaced log made the pass look dead. Neither belongs in a release: one printed every second, the other on every state change. What stays is event-driven and diagnostic in the ordinary sense: LSFG's initialise line, shader-cache hits and misses, load and ABI failures, and one FSR line per output-size change. |
||
|
|
da25cb84cc |
Android: call eeClampMode 4 Exact, the name the other frontends use
The desktop and Big Picture pickers landed the same tier as Exact, and GameIndex.md documents it under that name. Android was the only frontend calling it something else, which made the same setting look like two different ones depending on which screen the user was on. The translation key moves with the label rather than keeping the old name for a value it no longer matches, so the map stays alphabetical and there is nothing left to mislead the next reader. |