Repository navigation
Renderer, pacing, Android and texture pack work from bringing up God of War: Ghost of Sparta - #44
Open
arnaldolima1 wants to merge 29 commits into
Open
arnaldolima1 wants to merge 29 commits into
arnaldolima1 wants to merge 29 commits into
Conversation
A game that draws only part of a buffer and samples around it reads the pixels it never drew. On a PSP those hold what VRAM held, normally zeros; here a new target started undefined, and MoltenVK showed it as magenta. A bloom chain drawn side by side in one 512-wide buffer, each step at its own address, spread that magenta over the whole picture. New targets are now cleared to zero once, when they are first used. <prefix>_NO_TARGET_CLEAR restores the old behaviour. Also: <prefix>_TRACE_GE_LIST_WORDS sets how many words each traced display-list run prints (20000 before, and by default). Verified on macOS arm64 (Apple M4, MoltenVK) with God of War: Ghost of Sparta (UCES01401) under the interpreter: the title menu, magenta before, draws with its own colours. Not verified on Windows, Linux or Android, nor with any other game. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a vblank sceDisplaySetFrameBuf presented every call. The display reads the framebuffer at the vblank, so setting the same buffer again within one vblank period with nothing drawn since is no longer presented again (<prefix>_PRESENT_EVERY_FLIP restores the old behaviour). GameProfile::flips_wait_for_vblank, off by default: a game whose frame loop waits on nothing but the GE (which takes no emulated time here) flipped as fast as the host could draw, ten times a vblank, until the busy clock charged 100 ms at once and the pacer held the game for it: a hitch every few frames, emulation at 77% and audio underruns. With it on, a second flip within one vblank period waits for the next vblank (<prefix>_FLIP_UNLIMITED turns it off). Verified on macOS arm64 (Apple M4, MoltenVK) with God of War: Ghost of Sparta (UCES01401), recompiled: title menu at 60 fps and 100% speed, no frame over 20 ms, no audio underruns. With frame_vblanks left at 2 the presents stall (0 fps) while the game flips at 60; the profile sets 1. That interaction is not fixed here. Not verified on other platforms or with other games. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A profile's LauncherTheme puts a launcher before the game: a side bar with the game's logo and the menu's pages, a home page with a banner and quick settings, and a Play button. The menu draws its pages inside it, so every setting lives in one place before and during play. Its art is the player's: images in the data folder's launcher/ (or an app's Resources/launcher/), else PIC1.PNG and ICON0.PNG read from their disc image. --play skips it once, --launcher shows it once, and the player can turn it off. It backs the saves up when Play is pressed, keeping the newest ten. The theme's colours become the whole interface's: the widget palette is set at start instead of fixed, and text on an accent fill has a colour of its own. The interface speaks English and Brazilian Portuguese. Widgets look up what they draw in a table (ui/i18n.hpp); sentences built around names use patterns. The language is a setting (ui.language), the system's by default. GameProfile::adhoc_play false drops the Network page and the multiplayer rows for a game without multiplayer. The window is titled with the project's name rather than the executable's. Tested on macOS (Apple Silicon, MoltenVK) with God of War: Ghost of Sparta: launcher pages, Play, back, the in-game menu in both languages, checked from window captures. Not tested: Linux, Windows, Android, a gamepad by hand, touch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_apk.sh takes EXTRA_ASSETS, a folder whose contents go into the APK's assets; the app unpacks the launcher's images from it on its first start, beside the bundled font, where the launcher looks for an app's own. The touch face buttons were spaced 1.45 radii from the diamond's centre, so neighbours all but touched and a thumb pressed two. They are now a little smaller and 1.9 radii out, leaving more than half a button between them. A screen driven by touch has no Esc: the hints for Back and for closing the menu become buttons there, and the hints a finger does by touching the row itself are left out. With a mouse, clicking those two hints makes the request too. Tested on a Moto g84 5G (Adreno 619, Android 15) with God of War: Ghost of Sparta: the launcher with its art, the setup from the file picker, play, the face buttons and the menu's Back and Resume by touch. Not tested: other devices, the desktop click on hints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on a Moto g84 5G (Adreno 619) in God of War: Ghost of Sparta, whose bloom draws into corners of one buffer and samples other corners of it some thirty times a frame. The game ran at about 45 frames a second, 75% speed; with these changes it holds 55-58, 92-97%, in the same scene. - A texture read from a render target copies only what it reads, with a margin for filtering, instead of the whole target (<prefix>_NO_PARTIAL_COPIES copies all of it). - A target whose depth was never written neither loads nor stores depth in its passes, which halves what a pass moves on a tiled GPU; a draw that writes depth restarts the pass with depth kept (<prefix>_NO_DEPTHLESS_PASSES). - Checking that guest memory under a framebuffer texture is unchanged compares only the words under the texture, straight from guest memory, and a range compared once in a frame is spot-checked after that (<prefix>_NO_FB_CHECK_CACHE compares every word every time). The full comparison was 4 ms a frame on the phone. - The ignored GE commands are counted in a table rather than a map. - <prefix>_TRACE_PASSES logs every render pass and target copy. - A debuggable Android build reads <prefix>_* switches from env.txt in its storage, the only way to set them on a phone. Checked on macOS: the scenes before and after match pixel for pixel where two runs of one build do (runs differ from each other a little, the guest clock starting from the wall clock). Not tested: other GPUs, other games. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When drawing every frame would slow the game, frame skipping leaves some frames undrawn. Each flip measures what a frame took in real time, drawn or skipped, against the emulated time it stands for (one vblank, two for a game at 30 frames a second), and skips the share of frames that brings the average within it, spread evenly and never two in a row. A skipped frame runs the game and its display lists as ever, but draws nothing into the buffers the game shows nor into those drawn in the last two frames, so a buffer drawn once and kept (a reflection, say) is still made; the pointers move past the dropped prims undecoded. Its flip shows nothing new: the buffer flipped to holds a picture two frames old. Video > Frame skip (Auto, on by default; Off) and <prefix>_FRAME_SKIP turn it off; <prefix>_FRAME_SKIP_SHARE forces a share, to see it on a machine that keeps up. The performance line counts skipped frames. On a Moto g84 5G (Adreno 619) in God of War: Ghost of Sparta the game now holds 95-100% speed in its heaviest scene, showing 44-58 frames a second, where it ran at 75-97%; its cutscenes at 30 frames a second skip nothing. On macOS nothing is skipped but the odd frame of a load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tiled GPU loads and stores a pass's render area, and every pass covered its whole target. God of War: Ghost of Sparta draws its bloom chain into corners of three buffers, about twenty passes a frame, each over 2-27% of its target, with the scissor left at the whole screen. A pass now begins over what its target was drawn in this frame and the last: the union of each draw's reach, its scissor narrowed to the box around a through-mode draw's vertices (a pixel's margin) or to the viewport a transformed draw is clipped to. A draw outside the area begins the pass again over the union, which happens in a target's first frames. A target drawn for the first time takes all of it. <prefix>_NO_PASS_AREA covers whole targets as before; <prefix>_TRACE_PASSES prints each area. Checked on macOS by eye: the scenes match but for animation timing (runs differ from one another). On a Moto g84 5G the player felt it smoother; not measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each list enqueue, stall update and sync, with the real time it took and since the previous call. God of War: Ghost of Sparta builds a frame's whole list, enqueues it once and syncs on it within a millisecond, so an asynchronous GE would leave it little to overlap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A profile can list the disc ids of other releases with the same textures (GameProfile::texture_pack_releases). Their packs import and load as the game's own: a PPSSPP pack made for the North American God of War: Ghost of Sparta keys the European release's textures alike, but was refused because its folder is named UCUS98737 and its textures.ini lists no game. A pack copied by hand into textures/<other release> is read too, when the game's own folder is missing. On Android, Import texture pack opens the system's folder picker, which reaches Downloads and SD cards. The pack is found in the picked tree in the layouts the desktop import knows, copied into a staging folder with a progress bar and a cancel button, checked there, and put in place. The JNI folder listing now reports file sizes for the progress. GameProfile::default_volume sets the volume a new player starts with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… parts The stencil test. The GE keeps the stencil in the framebuffer's alpha; the renderer now keeps it in a stencil buffer beside the depth buffer (32-bit float depth with 8-bit stencil, else 24-bit; without stencil only on a device with neither, or with <prefix>_NO_STENCIL). Clears of alpha clear the stencil, on targets that have used it, so targets that never do keep their passes without depth. God of War: Ghost of Sparta marks surfaces with it and draws its bloom only where it marked; without it the bloom covered the whole screen. Framebuffers inside another's area. A framebuffer pointer inside the area of an existing target with the same row length and format is that target on the PSP, drawn at an offset; it is now drawn into it, moved by its offset, instead of into a target of its own. Ghost of Sparta draws each character's shadow map 64 pixels apart in one buffer, blurs the row as one texture and projects each map from its place: as separate targets the blur and the projections read empty or stale pixels, which showed as black sheets and oversized shadows. The checked guest words are read again at the first draw at such an address in a frame, and frame skipping drops its draws with its target's. <prefix>_NO_TARGET_ALIASES keeps a target for every address. Clamped parts of a target. A texture read from part of a target, by a 3D draw or a through draw whose coordinates leave it, is copied into an image of its own so clamping and repeating work at the texture's edges, not the target's. <prefix>_NO_FB_EXTRACTS reads from the target's copy as before. GameProfile::frame_writeback (default on) lets a game that never reads its own picture with the CPU skip copying the shown frame back into VRAM every flip (<prefix>_NO_FRAME_WRITEBACK to try it). Target snapshots read guest memory directly, which showed in phone profiles. Traces: <prefix>_TRACE_STENCIL, <prefix>_TRACE_ALIASES, extracts and aliases under <prefix>_TRACE_FB_TEXTURES, which now prints each distinct value an ignored GE command is given (the first sixteen) instead of the first four. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GameProfile::start_skips_movies: while a movie the game plays through sceMpeg has shown half a second, START ends it. The ring is emptied as the game fills it and the stream calls report no data, so the game's own player runs to the end of the file and moves on, as it does when a movie ends. Off by default; <prefix>_NO_MOVIE_SKIP turns it off for a game that has it on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Video setting, offered for games that draw 60 frames a second: every other frame is skipped, so the game keeps its full speed and the picture shows 30, with half the drawing work. Frame interpolation is off meanwhile, and the Frame rate and Frame skip rows say why. Nothing is skipped while the game makes 30 itself (movies, menus), which would leave 15. <prefix>_HALF_RATE. On a Moto g84 (Adreno 619) God of War: Ghost of Sparta then holds 100% speed in its heaviest early fight, where drawing every frame ran at 85-90%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game may draw a mesh as many small prims with nothing between them but the vertex and index pointers: God of War: Ghost of Sparta makes some 6500 prims a frame, nine vertices each, and the renderer did its whole per-draw work for every one before merging them back into some 450 Vulkan draws. GeState now keeps such prims together and hands them to the sink as one: triangles, strips and fans become one indexed triangle list, in the order the renderer expands them in, and unindexed sprites join as they are. A command that may change how a prim draws ends the batch (pointers, BASE and the bone matrices, applied as the vertices are decoded, do not), and so do a block transfer, a patch, the end of the list and its stall. Raw draws (GPU vertex decode) are sent alone. <prefix>_NO_DRAW_BATCH turns it off. Ghost of Sparta: 6600 -> 790 draws a frame reach the renderer, 450 -> 145 Vulkan draws. On a Moto g84 (boat fight, x2, frame skip off) the game thread's display list time drops from 8.7 to about 6 ms and the game runs at 100% speed, where it ran at 90-91%. Captures on macOS match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every word of an automatic unit is a dispatcher entry, so every instruction is a block the switch can enter and the C++ compiler keeps the whole guest state in memory between any two of them. With --sparse-entries, blocks are the analysis' block starts plus every branch and j target, and entries are only where control can arrive from outside a block: analysis seeds (functions, data and relocated code pointers), jal targets, the word after every call or link branch, branch and j targets in another unit, the first word of each unit, and every word the analysis never reached. Delay slots are neither. A unit entered with no entry at that address calls the interpreter (new CorpusHostApi::interpret, ABI version 2), which runs up to the next registered address, instead of stopping with "invalid internal function entry". Without the option the output is unchanged. Ghost of Sparta: 1,040,000 entries -> 93,774; two units compiled with clang -O2 for arm64 are 35-38% smaller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each unit entry took the AOT memory view by reference. Guest stores go through a byte pointer, which may alias anything, so the RAM pointer and limits were reloaded from the view after every store. The entry now copies the view into a local whose address never escapes and passes the shared one on to chained calls. Unit 0172 of Ghost of Sparta: 2756 of these reloads -> 122. --return-hints: a `jr $ra` in a function called by a same-unit jal (or linking branch) first tests those callers' continuations, which are labels, before the dispatcher's PC -> entry table and the indirect branch every return in the unit shares. Up to 16 per return. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CMake sets MSVC for clang-cl too, so the generated units and overlays got /O2 /Ob0, which under clang-cl turns off inlining of everything not marked always-inline. clang-cl copes with these units as clang does on the other platforms; cl.exe keeps /Ob0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list modes acted as if the player confirmed the first entry, so a game with save slots always saved to and loaded from the first one. They now show the listed saves over the running game, with each save's icon, title, detail and date (and, for saving, a new save first); the dialog stays VISIBLE until the player chooses, and the load, save or delete happens then. Saving over a save or deleting one asks first; backing out reports a cancel. The cursor starts where the request's focus field says. Without an interface the list modes behave as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The face button that answered the game's own "save?" prompt could still be down when the list opened, and the list took it for back on its first frame; the pad now counts only once every face button is released, as for navigation. Each save shows its slot number and its whole detail, and the footer how many saves there are. After the choice the panel says how it went (saving, saved or could not save; loading; deleting) for a moment while the frame stays held, which also covers the half-drawn frames the game shows as it comes back from the dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The texture mapping registers (0xC0 TMAP, 0xC1 TEXSHADELS) were ignored, so every transformed draw took its vertices' own UVs. God of War: Ghost of Sparta projects its characters' shadow maps onto the floor with the texture matrix (mode 1, position source), and the shadows came out far from the characters and much too large. Mode 1 now multiplies the projection source (model-space position, UV, or normal) by the texture matrix and divides by q per pixel; mode 2 maps the world-space normal against the two lights TEXSHADELS names. The matrix and the mode reach the vertex shader in the object block, which texgen draws now write as lit draws do. <prefix>_NO_TEXGEN uses the vertex UVs, as before. Through-mode tiles with the clamp wrap mode also stop sampling at the edge texels' centres, as the GE does, when read from a render target's copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GE ignores lighting in through mode, but the renderer gave a vertex without a colour the material colour only when the lighting enable was off, and the last 3D draw usually leaves it on. Such through sprites came out white. God of War: Ghost of Sparta clears its shadow maps with them, so each map got a white ring that showed as a dark rectangle around every character's shadow; stray white lines along edges went with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window scale too large for the display (x4 is 1920x1088 on a 1080p screen) made a window that covered the whole screen and looked full screen. The scale is now cut to what fits the display's usable area, at start and when the menu changes it, and a resized window is centred. The launcher plays launcher/music.mp3 from its folders (the player's own file; nothing ships one), looped, at a third of the game's volume, and not when the audio is muted. MP3 decoding is dr_mp3 (public domain or MIT-0), since the bundled FFmpeg on macOS and Android has no MP3 decoder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mod can put NAME.264 (raw H.264) in mods/movies of the data folder: when the game plays its movie NAME, each picture it decodes is replaced, on the GPU, by the same picture of the mod's movie, scaled to the internal resolution; the game's sound and anything it draws on top (subtitles) stay. The game's own pictures still go to guest memory. The replacement is read a piece at a time and decoded on frame threads (AvcDecoder::open(threads)), so a long movie at 1920x1088 costs little memory and holds 30 frames a second. Games that open files by sector (sce_lbn) are followed too: the disc image names the file at a sector (IsoImage::path_at), and the movie player looks the mod up when a movie starts. A Mods page in the menu and the launcher turns replacement movies on or off (video.movie_mods, on by default), opens the folder and lists the movies found. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Texture packs live in mods/textures of the data folder (an older textures folder is moved there once) and their rows move from Video to Mods. - START skipping a movie is a setting (video.movie_skip, on by default) on the Mods page, for games whose profile allows it. - The Mods page shows how many remastered movies were found, not a list. - Opening a folder goes through open_folder only, and its file:// URL is right for a Windows path (file:///C:/...): the buttons did nothing there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FileBrowser::Options::system_first: the system's file or folder dialog opens at once and its answer is the browser's; cancelling it cancels. The browser waits with "Open the window again", "Browse here instead" and "Cancel", and draws its own list only when asked or when there is no dialog (gamescope, Android). The texture pack import and the save import, export and backup use it. - The texture pack review: a heading that says whether the pack can be installed, the folder, and one line with its size and the release it was made for; the install buttons right after; the pack in use now at the end, only when there is one. Its texts are translatable, and the paths are mods/textures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A file in mods/files of the data folder stands in for the disc's file at the same path, matched without regard to case (the disc names its files in capitals; Android and Linux tell cases apart). Games that open their files by sector (sce_lbn) are covered too: a sector open that spans exactly one disc file, which IsoImage::path_at names, is that file. The replacement is a read-only host file that answers as the disc's would: its reads count as disc reads for fast loading, ioctl still gives the disc file's first sector, and sceIoGetstat and sceIoDopen give the replacement's size. Each replaced file is logged once. The Mods page gets a "Replaced files" row (setting mods.files, on by default, <prefix>_FILE_MODS) with the number found and a button that opens the folder. Also: save_chooser.cpp did not build with libc++, whose file times are a 128-bit count that std::to_string has no overload for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shutdown() destroyed the main descriptor pool before the render targets, framebuffer extracts, the held frame and the interpolation targets, each of which then freed its descriptor sets from the destroyed pool. On MoltenVK that corrupted the heap: the program crashed as it quit (Trace/BPT trap, or a SIGSEGV in MVKDescriptorPool::freeDescriptorSets with guard malloc), now and then, more or less often as other allocations moved the heap around. The pool now goes after all of them. Ten scripted quits in a row on macOS (M4, MoltenVK) exit cleanly, where before up to all of them crashed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new row on the Video page, Antialiasing (Off, FXAA; setting video.antialiasing, <prefix>_ANTIALIASING=off|fxaa), off by default. With FXAA the game's frame is copied into a sampled image at the internal resolution, smoothed into a second one by host/gpu/shaders/fxaa.frag, and that is scaled to the window in its place, so the bars, the level horizon, the overlay and the interface are untouched. The shader is this project's own: a contrast test on luma, the edge's direction from second differences, a walk along the edge both ways, and a resample moved across it by how near the pixel is to the closer end, with a blend for features thinner than a pixel. One pair of images serves every frame in flight (the barriers around each use order it after the last); it is made when FXAA is first on and again when the internal resolution changes. The level horizon's pipeline set-up is shared with it (create_picture_pipeline). Tested on macOS (M4, MoltenVK) with Ghost of Sparta: the same cutscene frame with FXAA off and on at x1 differs only along edges (the staircase along Kratos' shoulder is smoothed), with frame interpolation on as well. GPU cost not measured on a phone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game that sizes its files from the directory listing (sceIoDopen, which gives a replaced file's size) then opens them by sector asking for the replacement's length, not the disc's. Ghost of Sparta does: a sector open of that length is now the replaced file too. Tested by putting the French FRONTEND.BIN (11348 bytes) in place of the English one (10308): the title menu shows in French. <prefix>_SYSTEM_LANGUAGE (0 Japanese, 1 English, 2 French, ...) overrides the profile's system language, for a game with several languages on its disc. Ghost of Sparta then reads DATA/FRENCH and uses a font with French accents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in place GeState::execute read every command word through GuestMemory::contains and an out-of-line load32. It now reads them through a host pointer to a 1 KiB window of the list, looked up again when the list leaves it. <prefix>_NO_LIST_WINDOW restores the per-word reads. triangle_indices wrote through push_back, and a batched prim's indices were then copied again one by one to add the batch's base. append_triangle_indices sizes the output once and writes the indices with the base added; an unindexed prim with every position decoded skips the lookup and the clamp. Measured on a Moto g84 (Snapdragon 695), God of War: Chains of Olympus, 15 s of simpleperf in a fight, absolute cycles on the game thread: list reads 0.83 G -> 0.17 G, index building (triangle_indices + send_draw itself) 1.39 G -> 0.85 G, in a run with more draws than the baseline. Render tests pass, with a new test for the appended form; captures on macOS look right. Not verified: Ghost of Sparta, Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MHunterG
self-requested a review
October 19, 2026 06:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Framework changes made while bringing up God of War: Ghost of Sparta (UCES-01401) on PortableKit, in the port gow-ghost-of-sparta-pc. Each is something a PSP or another game can hit, not specific to that game. Behavior that changes for every game is listed with the switch that restores the old behavior. Behavior a game has to ask for is a
GameProfilefield that defaults to off.The 13 commits are independent enough to review one by one, and the commit messages give the details and measurements.
Renderer: correctness
0x24,0xDC,0xDD, alpha mask0xE9)NO_STENCILNO_TARGET_ALIASESNO_FB_EXTRACTSNO_TARGET_CLEARPerformance (measured on a Moto g84, Adreno 619)
Draw batching in GeState (
NO_DRAW_BATCH): prims drawn in a row with no state change between them reach the renderer as one draw (triangles/strips/fans as one indexed list, unindexed sprites appended). Ghost of Sparta makes ~6600 prims a frame of ~9 vertices; 790 now reach the renderer and the display-list time drops from 8.7 to ~6 ms, taking the heaviest scene from 90% to 100% speed at ×2 without frame skipping.Partial target copies, depthless passes on tiled GPUs, cheaper framebuffer-texture checks: 45 → 55–58 fps in the same scene. Off switches:
NO_PARTIAL_COPIES,NO_DEPTHLESS_PASSES,NO_FB_CHECK_CACHE.Render passes over only the area drawn, instead of the whole target (
NO_PASS_AREA).Frame skipping (Video → Frame skip, on by default): keeps a slow device at full game speed. Never two frames in a row; buffers kept across frames are still drawn.
Video → Limit to 30 fps (games that make 60): every other frame is skipped at full game speed, nothing while the game itself runs at 30. It holds 100% where drawing every frame ran at 85–90%.
GameProfile::frame_writeback(default on) lets a game that never reads its own picture with the CPU skip the per-flip VRAM write-back.Target snapshots read guest memory directly, which had shown up in phone profiles.
Pacing
A flip that repeats the same buffer within a vblank is presented once (
PRESENT_EVERY_FLIP).GameProfile::flips_wait_for_vblank(off by default) is for games whose frame loop waits only on the GE: they flipped ten times a vblank and then hitched.Interface, Android, texture packs
GameProfile::launcher, off unless a profile sets a theme): side bar, home page and Play button, with the menu's pages inside it.ui.language).GameProfile::adhoc_play: hides the Network page for single-player games.<prefix>_*switches fromenv.txt.GameProfile::texture_pack_releaseslets packs made for another release with the same textures import and load. A PPSSPP pack for UCUS98737 keys UCES01401's textures exactly. On Android, Import texture pack… now uses the system folder picker, copies the pack with progress and cancel, checks it and installs it. The JNI folder listing now carries file sizes.GameProfile::start_skips_movies(off by default): START ends ansceMpegmovie by reporting the stream as finished.GameProfile::default_volume.Traces
TRACE_GE_CALLS,TRACE_PASSES,TRACE_STENCIL,TRACE_ALIASES.TRACE_FB_TEXTURESnow prints every distinct value an ignored GE command receives (first 16), which is how the stencil use was found.Testing
Tested:
portablekit_texture_pack_tests(new cases for other-release packs),portablekit_settings_testsandportablekit_profile_testspass.portablekit_host_checkneeds installed test-game data and was not run.Not tested:
🤖 Generated with Claude Code