mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
master
25057
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
19335b2e54 |
LSFG: adaptive pacing captured the wrong target rate
Turning adaptive pacing on silently turned frame generation off: the OSD read FPS 60 / LSFG 60 on a 120Hz panel. The target was captured from Display.getRefreshRate(), which reports the rate the app's window is being driven at, not what the panel can do. Android leaves a window at 60 until something asks for more, so the stored target became 60 -- and the pacer then correctly asked for zero interpolated frames, since a 60fps game at a 60Hz target already satisfies it (desired_outputs = interval * target = 1.0, generations = outputs - 1 = 0). Derive from the highest supported mode at the CURRENT resolution instead, which is what the switch always claimed to mean. Same resolution filter used elsewhere, so the target can never imply a mode switch of its own. Existing installs need the switch toggled off and on to re-capture: the value is stored, not recomputed. Also swap the credit-card glyph on the library's Memory Cards row. |
||
|
|
d9b961eb36 |
Community batch: seven feature requests
Second screen (BrainBeat, NiceRon): - The panel picked its display by "not DEFAULT_DISPLAY", so launching ARMSX2 on the second panel put the panel on top of the running game. Anchor on the display the activity is actually on, and re-pick it on every resume rather than only on a foreground change. - Restyle the panel: it inherits the system dialog theme, not the app's, which is why it looked like a stock Android dialog. Dark ground, rounded tiles, one accent, painted in code since a Presentation is outside the Compose tree. - Customisable grid: SecondScreenTiles declares the tiles (stats, actions, macros, achievements, hide-panel), SecondScreenLayout stores which and in what order, and App settings edits both plus the column count. Stored by tile id, not ordinal. Achievement tile shows collection progress plus whatever unlocked this session -- the snapshot carries no timestamp, so "recent" is the locked-to-unlocked edge on the panel's own tick. Library (Isshin, GBSUPREMO): - Cover region per game, overriding the library-wide choice; "Library" is the absence of a pin, not a fifth region. Both cover components now subscribe to the region state -- game.coverUrl resolves it inside a plain getter, which Compose could not see, so cards kept their old art. - Memory cards reachable from the long-press menu. The card picker already did per-game assignment whenever handed a game; only the in-game menu ever handed it one. In-game (Sizor, Grayy): - Quick menu docks left or right. Alignment, slide direction, rounded corners and inset all move together. - Stats position (four corners), driving PCSX2's own OsdPerformancePos. Stored as the core's enum ordinal so there is no translation table to keep in sync. - Cycle-display-refresh hotkey, using preferredDisplayModeId -- the frame-rate vote in EmulationSurface is a hint the compositor may ignore, which is right for latency and useless as a user-facing toggle. - Analog Sticks section in the quick menu, extracted from PadTab the same way Gyro and Macros already were. Also drop the -fexceptions/no-PCH carve-out on GSLsfg.cpp: it existed because lsfg-vk-android reported failure by throwing, and the Eden port has no throw sites. |
||
|
|
b8f80aee02 |
Fix Reset leaving per-game settings, and Fast Forward (Toggle) on analog triggers
Two reported bugs, unrelated to each other. RESET (takanome9104, confirmed by lugnel): per-game settings such as affinity and GS multithreading survived a full reset. purgeAllSettingsFiles deleted PCSX2-Android.ini and gamesettings/ from currentInitDataRoot — one root. A device with a configured system directory has TWO, and on a device that has been moved between them gamesettings/ exists under both; the surviving copy is re-read on the next launch. Verified on the test device: gamesettings/ present under BOTH the SD root and app-private storage. Every known root is purged now. Deleting a file that is already gone is free, so casting wide costs nothing. This is the same single-root assumption that hid save states from the library's long-press menu earlier today. Worth suspecting wherever this codebase resolves 'the' data directory. The prefs clear also moved from apply() to commit(). restartApp calls Runtime.exit(0) on the next line, and apply() only guarantees the in-memory update — its disk write is asynchronous and an abrupt exit can beat it. A reset that survives the restart is the entire point of the button. FAST FORWARD (SKrazy on an AYN pad, Shmoda12 on a Thor): Fast Forward (Toggle) bound to L2/R2 came on for a frame and then reported OFF. It worked as Hold, it worked on a non-trigger button like R3, and it worked once the pad was switched to digital triggers. Those three facts together say it: some pads report a trigger BOTH as an axis and as a key event, so one pull reaches the hotkey dispatcher twice — once from sendTrigger, once from the key path. For a HOLD that is harmless, since both compute the same state from the same edge. For a TOGGLE the first flips it on and the second immediately flips it back. Digital triggers send only key events, which is why that setting 'fixed' it. The axis path now claims the press and the key path skips its own edge. Scoped to L2/R2 alone so nothing else changes, and cleared on release so the next pull re-arms. sendTrigger already carried a comment about pads that report triggers both ways — the hold path had been made safe against them, the hotkey path had not. |
||
|
|
cc9e58cf4e |
Library: port the Flurry animated background from ARMSX3
Calum Robinson's Flurry screensaver (2002, BSD-3-clause) as a live library backdrop, already shipping in ARMSX3. Source port, not the Windows .scr — every upstream copyright header is intact. Its own SHARED target rather than folded into the core, for two reasons: it is BSD next to GPL and that boundary should be visible, and it is plain C from 2002 that wants none of the C++20 the emulator is built with. gl_compat.c answers the GL 1.x calls the sources make — client-side vertex arrays, a fixed-function ortho, GL_QUADS — with a GLES2 shader, so the renderer stays unmodified. ★ The integration differs from the brief, which surveyed the refresh-experimental checkout. That tree has ui/GamesList.kt and no background system, so it needed a new full-bleed host. This one already has one: Flurry slots in beside XmbGlView under the same libraryBg == null branch and inherits its fallback — if GL cannot come up we get LibraryWaveBackground rather than a hole. onRelease stops the render thread, without which the EGL thread outlives the composition and keeps drawing to a dead surface. ★ add_dependencies had to move BELOW the emucore target. Stated next to the flurry target — which is defined earlier in the file — configure fails outright, because add_dependencies requires a target that already exists. The dependency itself is required: libflurry.so is loaded by System.loadLibrary and never linked, so nothing else in the build would make it, and a missing .so stays invisible until someone switches the background on and the view throws UnsatisfiedLinkError. Off by default, and said plainly in the description. It is a particle simulation rather than a still, and this library shipped a looping video once and lost it in 2.5.9 when the continuous decode turned out to cost real performance. Preset picker included, with random as the default choice. A UI preference, so prefs rather than the twelve-site Settings.kt path — it does not touch the emulator config. Verified: libflurry.so is packaged and exports all six JNI entry points. NOT yet run — Flurry's frame cost has never been measured on either project, and Water spawns nine flurries, so that number is still owed before it is recommended. |
||
|
|
8c0ae12398 |
Patch: stop Hardcore blocking presentation patches
Reported by EddyOP (60 FPS and Widescreen disabled under RetroAchievements
Hardcore) and diagnosed by Jetup, who found that moving the same lines under a
widescreen heading re-enabled them — ARMSX2 issue #541.
The Hardcore gate from
|
||
|
|
66aeaaeb96 |
Patches: cache the repository trees on disk
The other half of the online-browser complaint. Stopping the scan when you leave addressed the heat; this addresses the minutes. Every search downloads four GitHub '?recursive=1' listings — multi-megabyte JSON for repositories holding tens of thousands of pnach files — and regex-scans each for paths. The existing caches are in-memory only, so the first search after every launch paid the full price again, which is why it reads as broken rather than slow. The EXTRACTED PATH LIST is what gets cached, not the JSON: a fraction of the size, and coming back in it skips the expensive regex entirely, which is the CPU half of the cost rather than the network half. Seven-day TTL. These repositories gain files occasionally and the cost of being stale is one newly-added cheat not appearing, against re-downloading megabytes on every cold start. Keyed by a hash of the tree URL rather than a sanitised name, since two repositories can differ only in characters a filesystem folds. The cache directory is handed in rather than discovered: PatchRepo is a context-less object, so until setCacheDir runs it stays memory-only and behaves exactly as before. |
||
|
|
48094495a0 |
Patches: stop the online scan when you leave the browser
Reported by SNAKEATEROP (Helio G99): after using the online cheats/patches
browser, going back to the game left the device heating severely and a game that
had held full speed no longer did. Nothing in the emulator explained it.
The scan was UNSTOPPABLE, not merely slow. PatchRepo's fetch functions were
plain blocking calls with no isActive check, no ensureActive and not even
suspend. Kotlin cancellation is cooperative, so cancelling the scope did
nothing: the work ran to completion no matter what the user did. It walks four
community repositories, each a multi-megabyte GitHub tree that is downloaded and
then regex-scanned for paths — that is the CPU the game was competing with, and
it kept going long after anyone was looking at it.
Three parts:
· PatchRepo's entry points are suspend and check for cancellation between
every repository and every file. Between SOURCES is the one that matters —
that is where the time goes.
· The scan's Job is tracked, so a second search cannot stack on the first, and
the browser cancels it in onDispose. viewModelScope alone was not enough:
the ViewModel is Activity-scoped and shared with the settings tab, so it
does not clear merely because the user went back to the game — which is
exactly the case that was reported.
· The progress text now says it takes a minute or two AND that leaving is
safe. Users assumed it had hung, and several were told to just wait; nobody
should have to sit through it to protect their device.
This does not make the scan faster. It makes it stop, which is the part that was
damaging. Caching the repository trees on disk is the fix for the duration —
they are re-downloaded and re-parsed on every cold start today — and is worth
doing next.
|
||
|
|
879d07209c |
GS: size the texture cache as RAM minus a reserve, not a fraction of it
Third attempt at this budget, so the reasoning is written down properly.
Uncapped OOM-killed Android on a 5 GB uncompressed Persona 3 FES pack. Capping
at RAM/4, then RAM/2, stopped that and broke the same pack on 8 GB devices where
it had been working — 5 GB against a 4 GB budget evicts continuously, each load
dropping the previous one. That is the Persona 3 FES report: corruption first
(a failed upload injected with undefined contents, before
|
||
|
|
7bbe5b2fc1 |
GS: remove the texture-replacement cache cap
Reverses the 2026-07-20 policy. Reported by JustVibin247 for Persona 3 FES: mods worked on 2.6.6 and stopped on 2.6.6.1, first showing as corruption and later as simply not applying. The cap was added to stop a 5 GB uncompressed Persona 3 FES pack OOM-killing Android mid-load, and it did stop that. It also broke every setup where an oversized pack had been working. Budget was RAM/2, so on an 8 GB device that same 5 GB pack sat permanently about 1 GB over and evicted continuously — each load immediately dropping the previous one. That produced both reported symptoms in the order they were reported. The churn means constant re-upload; before |
||
|
|
aba5f201e6 |
Saves: hiding Save must not hide Load
Save and Load shared a single guard, so gating Save on a running VM hid Load with it — and Load is the entire reason the library's long-press menu opens this screen. They have different conditions and now have different guards. Load needs only that the state belongs to the game in context: with no VM it boots the game and loads into it. Save additionally needs a live VM, because there is nothing to snapshot without one. |
||
|
|
2443ad72cf |
Saves: only offer Save when a VM is actually running
Opened from the library's long-press menu the Save Manager showed a Save button next to Load, which cannot mean anything: nothing is booted, so there is no running game to snapshot. The button was gated on canUseWithActiveGame, which means 'this state belongs to the game in context' — a different question, and one that only started answering true here when contextGame was wired up in the previous commit. Saving needs a live VM; loading does not, because it can boot the game first. The two conditions were conflated and the fallback fix exposed it. Now gated on hasActiveVm, read from NativeApp.hasActiveVM() during refresh. Load and Delete are untouched: both are meaningful without a running game. |
||
|
|
208bf68f1b |
Saves: honour contextGame when listing, not just when launching
Long-press -> Load save state opened the Save Manager but nothing could be loaded from it. load() already fell back to contextGame; the LISTING did not. It read currentGame alone, which is null when nothing is booted — so every entry came back canUseWithActiveGame = false, which is what greys out Load, and the serial filter also stopped applying so the screen showed every game's saves at once rather than the one that was long-pressed. contextGame was added for exactly this case and the launch path already used it. The read here had simply never been updated, because until now the only way into this screen was from a running game or the drawer. importSaveStateToNextFreeSlot deliberately still requires currentGame: it resolves destination paths through NativeApp.getGamePathSlot, which answers for the running VM, so a context game would give it nowhere to write. |
||
|
|
146da3d2ef |
Library: open the real Save Manager, and make swipe-to-dismiss actually work
Both from testing feedback. The save-state feature itself worked — slots listed and booting into one loaded correctly — but two things around it did not. ★ The swipe did nothing, and the reason is worth writing down: Compose delivers pointer events to CHILDREN first. Every row in the sheet is clickable, so they consumed the drag in the Main pass and a detector on the parent Box never saw it. Watching PointerEventPass.Initial is the only way a parent wins that. Winning it everywhere would be worse than not having it — it would eat scrolling inside any modal that scrolls — so the gesture is claimed only when it STARTS in the top 64dp, where the drag handle is and where a sheet is grabbed anyway, and only once it has clearly travelled downward. The absorb-taps clickable also moved AFTER the detector; having it first gave it the events. This mattered more than a missing nicety: the game menu is nearly full-height, so there is almost no scrim left to tap, and without the swipe the only way out was the controller. A touch user was stuck. ★ 'Load save state' now opens the Save Manager rather than a bespoke list. That was the request, and it is also the better implementation: the Save Manager already renders slots as a grid with preview thumbnails and carries its own back button, so it cannot trap anyone. contextGame exists for precisely this — it is how the Save Manager already operates on a game that is not currently running — so this is wiring, not new UI. The bespoke picker and its modal are deleted. SaveSlotLookup stays: the menu still needs to know whether a game has any states at all, to decide whether to show the row. |
||
|
|
caaf80749e |
Library: find save states in both data roots, and restore swipe-to-dismiss
Two follow-ups from testing the long-press menu. ★ SaveSlotLookup only searched ONE root. A device with a configured system directory has two — assetCopyRoot resolves to that one (typically the SD card, where ROMs and most saves live) while others stay under getExternalFilesDir. On the test device 13 states sat in one and 5 in the other, so either root alone under-reports, and the failure is silent: it reads as 'this game has no save states' rather than as a bug. This is the same two-root trap that once made a patches investigation report a false 'clean'. Both are searched now, and when a slot exists in both the newer file wins. Swipe-to-dismiss is back for bottom-aligned modals. PadModal replaced ModalBottomSheet because that is its own focused Android window and every row inside it was unreachable by pad; the swipe was the one thing given up in the trade. But a panel that rises from the bottom edge with a rounded top and a drag handle is PROMISING a swipe, so its absence reads as broken rather than as a deliberate omission. The panel now follows the finger and dismisses past a threshold, without giving up focus ownership. Downward only, and only for BottomCenter: dragging a bottom sheet up should not lift it off the edge it is anchored to, and on centred or anchored menus a vertical drag means nothing and would fight scrolling inside them. Worth recording that the originally reported symptom was NOT a bug: God of War II has no save states on the test device, so an absent row was correct. The root bug was real but found by inspection while checking that. |
||
|
|
0f0f719bce |
Library: fix the selection highlight contrast, and offer save states on long-press
bmdhacks' two. Selection highlight was blue on blue. It drew a single ring in the theme's primary, and the library background is themed from the same palette — so on a blue theme the highlight was invisible, which matters because controller navigation is the only way that selection is moved. Now two rings: an outer one derived from inverseSurface, which contrasts with the background whatever hue the user picked, and the accent ring inside it. Whichever the background happens to match, the other still reads. The list rows had the same problem and get a thicker stroke blended toward inverseSurface. Long-press already opened the game menu; it now offers the game's save states and boots straight into one. Most of that already existed and only needed connecting: pendingSlotLoadOnBoot and launchCurrentGameFromSaveSlot have been driving the Save Manager's 'relaunch and load' for a while. The one thing that did not work from the library is that it resolved the game from currentGame/contextGame, and in the library nothing is booted so both are null — it could never have fired there. Split into launchGameFromSaveSlot(game, slot) which names the game explicitly. Enumerating the slots needed new code for the same reason: the in-game picker asks NativeApp.getGamePathSlot, which resolves against the RUNNING VM's serial. SaveSlotLookup reads the files instead, using the layout the save manager already walks — '<serial> (title).NN.p2s' under sstates/ or savestates/. The row only appears when states exist. A 'Load save state' entry that opens onto an empty list is worse than no entry. The picker is a second PadModal rather than a submenu inside the game menu: PadModal owns focus, so nesting one inside another leaves the inner rows unreachable by controller — the same trap that made the game menu a PadModal instead of a ModalBottomSheet in the first place. Both flavours compile. Not yet exercised on device. |
||
|
|
f6ddff79bc |
LSFG: expose adaptive frame pacing, and remove the diagnostic tracing
The pacer shipped in the previous commit but was inert: GSConfig.LsfgTargetRate defaulted to 0, which means "hold the multiplier fixed", and nothing in the Android settings could change it. So the port ran, but the specific problem it was brought over to solve — games that oscillate between 60 and 30fps on a 60Hz panel, where a fixed multiplier presents 120 then 60 and judders at every transition — was still there. Working but inert is the failure mode worth naming: nothing errors, the feature simply does not do the thing it was for. Plumbed through the usual twelve places (the field, INI read and write, the differs chain, toJson/fromJson, both per-game override paths, the reset list, the search index, the strings, and the two call sites), plus the two C++ ones in Pcsx2Config. Presented as a switch rather than a number. The pacer needs a concrete Hz, but picking one by hand is not a decision anyone can make usefully and the only sensible answer is the panel's own refresh rate — so the UI writes that when the toggle goes on, and 0 when it goes off. Off remains the default, so behaviour is unchanged until it is asked for. Also removes the step tracing added while chasing the Turnip crash. It did its job: five rounds of reading the code produced three wrong theories, and the trace produced the answer in two. The reasoning it uncovered is in the comments, which is where it belongs — the instrumentation is not. The new strings live in the github-only table, so the Play split still holds: playDebug has zero class files containing 'Lossless' or 'perf.lsfg', githubDebug has 2 and 4. |
||
|
|
2080bd1c44 |
LSFG: make frame generation actually run on device
Verified working on an Adreno 740, on BOTH the stock Qualcomm driver and Turnip:
an interpolated frame presented for every rendered frame, no crash.
Six real defects between 'compiles' and 'runs', all mine. Recording them because
every one of them compiled cleanly and several looked like somebody else's bug.
★ THE LAST ONE, and the least guessable. Waiting on a semaphore signalled by a
SECOND vkAcquireNextImageKHR within one frame segfaults inside Turnip at
vkQueueSubmit. Stock Qualcomm accepts it; Turnip does not. The extra acquire now
signals a FENCE which we block on before recording, so the submit waits only on
the caller's render-finished semaphore — exactly what a non-generating frame
does, and that shape always worked. It costs a short CPU stall per generated
frame, still far cheaper than the two full device idles per frame the old
implementation paid.
The tell was in the trace, not in the code: working frames submitted
waits=1 signals=1, the frame that died submitted waits=2 signals=2. Everything
else about that frame — the dispatch, the copy, the fences, the presents — was
identical.
The other five:
· __fi on a free function in a header. PCSX2's __forceinline is
__attribute__((always_inline, unused)) with NO inline keyword, so every
including TU emitted its own copy: duplicate symbol at link, from a header
that compiles perfectly alone. Pcsx2Defs.h provides __forceinline_odr for
exactly this and the rest of the renderer only uses __fi inside class
bodies, where members are implicitly inline.
· Acquire budget. Vulkan allows imageCount - minImageCount + 1 images held at
once and the presented frame already holds one, so with min=3 and 3 images
the budget was ZERO. Acquiring anyway is undefined behaviour, not a failed
call. GetImageCount() - 1 was simply the wrong bound.
· Swap chain image count. Asking for base + 1 does nothing: on FIFO the base
is 2, so it clamps straight back up to minImageCount. The request has to be
anchored to minImageCount, or the budget stays zero and frame generation
silently never runs with nothing reporting an error anywhere.
· One command buffer, one semaphore set, no fence. Resetting a buffer that is
still executing and resubmitting one that is still pending are both
undefined. The OLD implementation had the same single-slot arrangement and
got away with it because it called vkQueueWaitIdle twice a frame — removing
those idles is the entire point of this port, and it removed the accidental
serialisation that made reuse legal. Now one slot per swap chain image, each
with its own fence.
· Initialisation order. Moving image allocation into CreateResources without
moving the allocator ahead of it dereferenced an empty std::optional and
killed the GS thread during BIOS boot — before frame generation would ever
have produced a frame, so it presented as an entirely unrelated crash.
Generated frames also now go into images WE own and are copied into the acquired
swap chain image, rather than being dispatched straight into it through a
storage view. That theory did NOT fix the crash — but it is what both Eden and
the old implementation do, it asks nothing unusual of the WSI, and it let the
swap chain drop VK_IMAGE_USAGE_STORAGE_BIT entirely, which removed the
'enable it, then restart the renderer' wart along with it.
Diagnosis was step-tracing the present path, not reading it: five rounds of
reading produced three wrong theories, and the trace produced the answer in two.
The instrumentation is removed; the reasoning is in the comments.
|
||
|
|
0bfefd4b69 |
LSFG: run frame generation on our own device, and delete the old path
Completes the switch to the Eden port. GSLsfg keeps its entire public surface — availability, status text, display FPS, the settings and OSD plumbing all untouched — and only its internals change, so nothing above the renderer had to move. What actually changed on screen: the old implementation ran the interpolator on a SECOND VkDevice and shared images as AHardwareBuffers, and because Android offers no cross-device semaphore (Turnip rejects OPAQUE_FD export on AHB memory) the only barrier available was a full device idle — twice per frame, every frame. That is gone. Generation is now ordinary compute recorded into a command buffer on the device we already have, and interpolated frames are written STRAIGHT into an acquired swap chain image through a storage view, so the intermediate copy is gone too. The pacer comes with it, which is the fix for games that oscillate between 60 and 30fps on a 60Hz panel: the generation count now varies to hold the presented rate near a target instead of blindly multiplying whatever the game produced. ★ ONE submit, N+1 semaphores. All the generation work goes into a single command buffer, submitted once, waiting on the caller's render-finished semaphore plus every acquire, and signalling one semaphore per present that follows. The obvious alternative — a submit per generated frame — walks straight back into the binary-semaphore bug this file was bitten by before, where the real present and the first generated present both want to wait on the semaphore that says the source has been read. A binary semaphore may be waited exactly once. ★ The hook fires AFTER vkQueueSubmit, so FrameGen had to take its command buffer as a parameter. It was written against GSDeviceVK::GetCurrentCommandBuffer(), which at that point is in flight or already belongs to the next frame; recording into it is undefined and the symptom would have been interpolation running a frame late rather than anything resembling an error. Layout bracketing is ours: the ported passes speak Eden's convention where a presentable image lives in GENERAL, and PCSX2 hands them over in PRESENT_SRC_KHR and needs them back in it. The swap chain now requests VK_IMAGE_USAGE_STORAGE_BIT — but only when frame generation is on AND both the surface and the chosen format allow it. Asking unconditionally fails swap chain creation outright on drivers that do not, which would take the whole renderer down for a feature that is switched off. The format half is the easy one to miss: a surface can report STORAGE support while the sRGB format picked for it has no STORAGE_IMAGE feature bit, and that only shows up later as a validation error at image-view creation. Because usage is fixed at creation, switching the feature on mid-session needs a renderer restart; Initialize says so rather than failing silently. DELETED: platforms/android/app/src/main/cpp/3rdparty/lsfg in full — the lsfg-vk-android framegen library, the DXVK dxbc compiler, pe-parse, volk and its 759-symbol collision with VKLoader, the C ABI shim, the version script, the separate .so and the dlopen that found it, and the -fexceptions carve-out they needed. GSLsfg.cpp went from 1259 lines to 654. The ~130 MB configure-time fetch goes with it. build-play-aab.sh's guard was rewritten rather than dropped: it checked for a file that can no longer exist either way, so it would have passed forever without proving anything. It now looks inside the core for a symbol only the ported implementation defines. Verified: all 18 affected translation units compile without errors, with ARMSX2_HAS_LSFG on AND off (the play flavour still compiles the feature out entirely). Not yet run on hardware. |
||
|
|
5e1d979b4e |
LSFG: port Eden's frame generation (passes, pacer, DLL reader)
Ports the frame-generation implementation from Eden (eden-emu PR #4263), which is a substantially better design than the lsfg-vk-android one we currently ship. Why it is better, concretely. Ours runs framegen on its OWN VkDevice, shares images through AHardwareBuffer, and — because Android gives no cross-device semaphore, Turnip rejecting OPAQUE_FD on AHB memory — uses full device idles as its only barrier. Eden's runs as ordinary compute on the device we already have. It also needs none of what ours drags in: no DXVK dxbc compiler (its shader translate is a SPIR-V validate plus a descriptor-binding renumber, because current Lossless.dll ships SPIR-V in its RCDATA resources), no pe-parse, no volk and its 759-symbol collision with VKLoader, no separate .so, no C ABI, no dlopen, and no -fexceptions carve-out. It also brings a real frame PACER, which is the answer to games that oscillate between 60 and 30fps on a 60Hz panel. A fixed multiplier presents 120 then 60 there and judders at every transition; the pacer varies the generation count to hold the OUTPUT near a target instead. New GSConfig.LsfgTargetRate drives it, defaulting to 0 = the existing fixed-multiplier behaviour, so this is opt-in. Nothing is wired up yet — GSLsfg still drives the old path. This commit is the ported library only. ★ The load-bearing decision is LsfgVkCompat. The pass code is written against yuzu's RAII wrapper and its Device/MemoryAllocator, which PCSX2 has no analogue for. Rather than rewrite ~2000 lines of call sites, the slice of that API the code actually uses is reimplemented over PCSX2's raw handles and VMA — it came to five command-buffer methods, three Device queries, two allocator entry points and eight handle types. The result is that every pass body is BYTE-IDENTICAL to Eden's, so upstream fixes stay a readable diff instead of a merge puzzle. Deliberate departures, each commented at the site: · paths are std::string, not std::filesystem — the GS backend uses neither · CityHash -> GSXXH3_64bits, already used elsewhere in GS · the shader cache gained mtime + a flags field so a hit costs a stat() rather than a full read, hash and PE walk of the DLL on every launch; Eden keys on a content hash and so must read the whole file before it may look at the cache. GSLsfg.cpp already validates on size+mtime, so this matches the tree. · Eden's RemoveInstalledLosslessDll() is NOT ported. It deletes the DLL, which is safe there because Eden owns that file; here the path is whatever GSConfig.LsfgDllPath says and nothing checks it points inside our storage. Only the cache half is kept, as ClearShaderCache(). · vk::Buffer gained Flush(). The port initially dropped Eden's flush because the shim had nothing to flush through. That write is the shader's entire uniform block, and the failure mode is not a crash — it is interpolation reading stale constants, which reads as a motion artefact, not a bug. Verified: all 14 translation units compile clean against the real PCSX2 headers under -Wall -Wextra. The reconstructed util.cpp helpers were diffed against the genuine Eden source fetched from the merge commit — the extracted diff hunks in the working copy are PARTIAL, added lines only, so they were not safe to trust. |
||
|
|
517fa69c4c |
OSD: stop any settings change from wiping the active OSD mode
Changing any setting at all — brightness, a speedhack, a controller binding — made the on-screen display disappear. The OSD has two independent controls that both write the same native flags. The per-stat selection in settings, and the MODE picked from the in-game menu or the hotkey (Full / Minimal / Custom / Off). Settings.applyTo() pushes the per-stat osdShow* flags unconditionally, and applyTo runs on EVERY settings change, so it was overwriting whatever mode was active with the Custom flag set. On most setups the Custom set is mostly off, which is why the symptom reads as the OSD vanishing rather than as it changing. The mode STATE was never lost — InGameOverlay.osdMode still said Full, and the in-game menu still showed Full. Only the native flags had been replaced, so the UI and the screen disagreed and nothing looked wrong from the app's side. Fixed at the applyTo choke point rather than at its five call sites: a re-assert that reapplies the mode when it is anything other than Custom. Custom is left alone deliberately — applyTo has just written exactly what Custom means, and re-applying would be a redundant round trip through the CPU thread. This is the same shape as the boot-time applyStoredOsdMode() and the second-display reapplyOsdMode(), which already restore the mode after something else has pushed flags underneath it. applyTo was the third place that needed it and the only one that had no such guard. |
||
|
|
54d2850295 |
LSFG: keep it out of the Play build entirely, not just switched off
Play builds cannot carry LSFG at all, and they did. The gating was a
BuildConfig.LSFG check inside shared files, which is a weaker claim than it
reads as: the rows were never drawn, and all 22 frame-generation strings still
shipped in the Play dex in plain text — including "Lossless Scaling",
"Lossless.dll" and the requirements dialog naming the product, which is exactly
what a text search over the artifact finds. The native half was already
genuinely compiled out (-DARMSX2_ENABLE_LSFG=OFF); only the Kotlin half looked
like it was.
Moved to source sets, which is the arrangement that actually excludes:
LsfgSection.kt main -> github, with a no-op stub in play
the 22 EN strings -> I18nLsfg.kt, real in github and an EMPTY MAP in play
the 5 search rows -> SettingsSearchLsfg.kt, likewise
LsfgEmulationCard new, so the shared pause-menu file no longer even names
the section's string key (SectionCard became internal)
EN is now BASE_EN + LSFG_EN and the search index BASE + LSFG, so whichever
flavour is in scope supplies its half and no caller knows which build it is in.
Splitting the search rows is a behaviour fix as well: in the play build they were
indexed while the section they pointed at was compiled out, so searching would
offer a result that rendered its own key as its title and led nowhere.
The settings FIELDS stay shared on purpose — identifiers rather than product
names, and an identical config schema across flavours is what lets a config move
between builds without losing data.
Verified on compiled output rather than source: playDebug has zero class files
containing 'Lossless' and zero containing 'perf.lsfg'; githubDebug has 2 and 4.
I18nLsfgKt.class is 3633 bytes in github and 833 in play. build-play-aab.sh now
greps the AAB's dex for both strings and fails the build if either appears, so a
later edit to a shared file cannot quietly undo this.
★ That verification first came back clean for BOTH flavours, which was a false
negative: Xcode's strings(1) parses a .class as a Mach-O fat binary, errors, and
prints nothing — indistinguishable from a pass. LC_ALL=C grep -a is what the
check uses, and what the comment in the script warns about.
|
||
|
|
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. |