The loader thread worked out the hash a texture's replacement is filed
under itself: from video memory, which with threaded rendering the game
is writing again by then, and from the texture's own fields, which the
renderer may be updating for the next request. It also looked at the
texture's "dirty" stamp, a plain variable of the renderer's.
The renderer does the hashing now, when it asks - it is updating the
texture at that moment, and the game is held while it does - and leaves
the three names for the loader in the texture before it counts the
request. The loader reads the count and then the names, loads, and
takes its count off: if a request came in meanwhile, the count it took
off is not all there was, it goes round again, and what it loaded from
names that may have been half one request's and half the next's is
dropped. The image and its size are still only looked at by the renderer
once the count is back to zero. Nothing else of a texture is read by the
loader.
(The "dirty" check saved loading for a texture that had been invalidated
since it asked. Such a texture asks again when it is next updated, and
the image loaded for nothing is freed then, or with the texture.)
tools/threads: the model of the loader's queue carries a three-word
request the same way and checks that what was made of each node's last
request is that request's, whole; under TSan and ASan + UBSan.
Run: the test disc with a pack of sixteen magenta replacements for the
textures it dumps, threaded rendering on - the screenshot has 41,148
magenta pixels against 24,228 without the pack; with the option on and
no pack, 24,228.
Not done: the audit's bound on what is queued. A texture is on the list
at most once and holds at most one loaded image, so both are bounded by
the number of textures, not by bytes.
Shenmue's Space Harrier mini-arcade loads a planar texture that
starts inside video memory and would end outside it, and uses only
the top. The texture cache reads as many lines as there are.
It did that by cutting the texture's height and size down for the
update and putting the height back afterwards. The size stayed cut -
it is what is locked and hashed, and has to end inside video memory -
so the second time the texture was updated nothing ran past the end
any more, nothing was cut, and every line was read, the ones beyond
video memory included. The first update, for its part, converted into
a buffer made for the cut height and then uploaded the full one.
The number of lines to read is now worked out each time from what the
whole texture would take, the height is left alone, the buffer is the
texture's size, and the lines that are not read are cleared. From
upstream ("fix Space Harrier arcade in Shenmue"), which restores the
size after each update instead; here the size has other readers.
The headless and live tests pass and the converter test's 340 results
are unchanged. No texture on the test disc runs past the end.
The option chose between two ways of treating what a game renders to
a texture: copy it to video memory after every such render, waiting
for the graphics card each time and losing Render To Texture
Upscaling, or leave it on the card and have the games that look for
it in video memory come out wrong. A table switched the first on for
the games known to need it.
Since the last three patches there is one way, and it is right for
both: the picture stays on the card and video memory gets it when it
is first touched there. Nothing is left for the option to choose.
- The core option, in every language, and settings.rend
.RenderToTextureBuffer.
- The copy-after-every-render paths of all four renderers, with what
only they used: the Vulkan drawers' second render target and its
render pass variant, the check for the setting having changed, and
WriteTextureToVRam() (rend/rtt_watch.cpp packs pixels now).
- The table's column, in the console's table and the arcade one, and
the 51 rows that had nothing else in them.
Render To Texture Upscaling stays, and now applies to every game.
A frontend that still has the old option saved ignores it.
The live test passes on gl, glcore and vulkan, the headless test
passes.
A paletted texture can be left as palette indices and looked up in the
palette by the shader. The shaders have one place in the palette to
look from, the polygon's first texture's. A polygon with two textures
("two volumes", the second drawn inside a modifier volume) whose
second is paletted had that one looked up from the first one's place -
or, when the first has no palette, drawn as its indices.
The second texture of such a polygon is now always converted to
colours, like any texture, and the per-pixel shaders, the ones that
do two volumes, sample it as that. From upstream ("convert palette
textures for area 1 on cpu").
Nothing changes yet: because of the mistake the next commit fixes, no
texture is left as indices at all. With that fixed and this not, the
live test's two-texture polygon is black in its volume where it has
to be magenta, in the per-pixel OpenGL renderer; with both it passes
on gl, glcore and vulkan.
Two small ones from upstream:
- a framebuffer shown without interlace on a television is 240 lines
at most, whatever its size register says. Upstream: the Echelon
intro of Confidential Mission (PAL).
- writing a render back to video memory as 1555, the alpha bit is set
from the threshold up, not above it. Upstream: a black tile under
the player's feet in Izumo.
And a framebuffer a game draws itself is converted for the GPU in the
memory textures are converted in, in place of 1.2 MB allocated and
freed every frame.
Every texture update allocated a buffer the size of its texture,
converted into it, handed it to the GPU and freed it; a compressed
mipmapped one allocated a second, of four pixels, for its smallest
level. The conversion now goes into one piece of memory that is kept
from one texture to the next and only grown (at most a 1024x1024
texture with its mipmaps, 5.6 MB), freed with the renderer; growing
it copies nothing, since what is in it is the last texture's. The
four pixels are a local. An update allocates nothing.
On this machine the time is the same - the C library was handing the
same block back each time. What is gone is the allocation itself,
which is not free everywhere.
Two things about planar textures and their stride, found on the way:
- with a stride wider than the texture, the last row was converted
past the end of the buffer by the difference. There is room for it
now. Upstream has the same overrun.
- with a narrower one, the rest of each row was never written and
went to the GPU as whatever the memory held. It is cleared.
The hash of a compressed texture is taken in two pieces, the codebook
and the texture, which with mipmaps are not next to each other. The
state that carries a hash from one piece to the next was allocated and
freed for every texture hashed; it is a local now.
- A compressed texture's size was half its pixels' share and left the
codebook out. The size is what video memory is watched by: a write
to the rest of the texture went unseen and the texture stayed as it
was. It is the codebook and a byte for every four pixels.
The size was also what the texture's name for custom textures was
hashed from, so two compressed textures could share a name. The
name is now the hash of the codebook and of the whole texture, the
same as upstream's; the old name is kept as a second one to look a
custom texture up by, so texture packs made before still load.
- A texture both compressed and planar was taken for a mistake and
its compression ignored. It is decoded as what it is: an entry of
the codebook is four pixels in a row. Upstream: the Genesis emulator
in the Teenage Mutant Ninja Turtles collection.
- Compressed and mipmapped, a texture is twiddled whatever the scan
order bit says. Upstream: corrupted textures and a crash in Star
Wars Demolition.
- The 1x1 level of a compressed YUV texture is taken from the 2x2
level; its own bytes are not a colour. Upstream: magenta background
textures in World Series Baseball 2K2.
- A stride of zero in TEXT_CONTROL means the texture's own width, not
a texture of no size.
The converters themselves were compared with upstream's first, each
one on the same bytes, and are the same; the new planar compressed
ones were checked the same way (tools/rend, next commit). The live
test passes on gl and glcore; the Soul Calibur state, which is mostly
compressed textures, runs with no texture refused.
One difference from upstream is kept: for a paletted texture the name
still includes the bit upstream leaves out, so the names this core
has given such textures do not change.
The linker was asked which functions of the core no code path reaches
(-ffunction-sections with --gc-sections --print-gc-sections), and each
one it named was then looked up by name across the whole source,
every platform's code and the tools included. These have no caller
anywhere:
MemWrite32 hw/pvr/pvr_mem.cpp
pvr_ReadReg hw/pvr/pvr_regs.cpp
rend_framePending hw/pvr/ta_ctx.cpp
rend_init, rend_resize hw/pvr/Renderer_if.cpp
emit_WriteCodeCache hw/sh4/dyna/driver.cpp
bm_WriteBlockMap hw/sh4/dyna/blockmanager.cpp
dec_Un_rNrN, dec_Un_frNfrM, dec_rz hw/sh4/sh4_opcode_list.cpp
cpu_iWarn hw/sh4/interpr/sh4_opcodes.cpp
get_nvmem_data hw/holly/sb_mem.cpp
aica_Term hw/aica/aica_if.cpp
reios_term reios/reios.cpp
cryptoReset hw/naomi/decrypt.cpp
printtoc imgread/common.cpp
dc_start nullDC.cpp
shutdown_thread_pool rend/TexCache.cpp
gl_DebugOutput rend/gl4/gles.cpp
libPvr_GetRenderTarget,
libPvr_GetRenderSurface libretro/libretro.cpp
VArray2::LockRegion libretro/common.cpp
LogManager::SetEnable log/LogManagerLibretro.cpp
set_user_data_dir,
add_system_config_dir,
add_system_data_dir stdclass.cpp, with the three
variables only they wrote
The two *_Term functions were empty. cryptoReset was not: but the
cartridge decryption it would have cleared starts itself afresh
whenever a key or an address is set.
Left alone, though the linker named them on x86-64: what the ARM,
ARM64 and x86 recompilers and the cached interpreter call (rdv_*,
emit_Write32, emit_SetBaseAddr, bm_GetStaleBlock, the block reference
counting, _vmem_ReadMem8SX32/16SX32, shil_opcode_name), cpu_iNimp,
which a build without an MMU calls, mmu_QACR_SQ, which is called but
inlined so that only its separate copy goes unused, and
archive_num_entries, which tools/archive uses.
libARM_Term, also empty and never called, stays for now: its file has
CRLF line endings, and a patch that touches it does not go through a
plain git am.
A number of settings are given a value once, in LoadSettings(), and
never another: there is no core option behind them and nothing else
writes them. Each is replaced by its value where it was read, and the
branch that value never took is removed.
- dynarec.idleskip, always on: the idle loop detection in the block
decoder is unconditional, and the branch for it being off is gone.
- dynarec.unstable_opt, always off: the store queue shortcut in the
ARM recompiler that it guarded is removed.
- pvr.MaxThreads, always 3: a constant in the texture upscaler.
- pvr.subdivide_transp, always off, and only read inside an #if 0
block of the triangle sorter, which goes with it.
- rend.Clipping, always on: tile clipping no longer asks.
- rend.AutoExtraDepthScale, always on.
- debug.SerialConsole, always off: the serial port's echo to stdout.
- imgread.PatchRegion, never set: the two functions that would have
rewritten a disc's region strings returned at once, and are removed
with the calls to them.
- validate.OpenGlChecks and pvr.Emulation.zMin/zMax were written and
never read.
LoadSettings() also loses a block for builds that are not libretro,
which this core is not, and which set a field that no longer exists.
core/rend/soft/softrend.cpp, a software renderer that used one of
these settings, was not in any makefile. It is deleted, and its
declaration with it.
A texture's control word has two bits, scan order and stride select,
that mean nothing for a twiddled texture, and the cache cleared them
when it took a texture in. For a paletted texture those two bits are
something else: the top two bits of the palette selection, which says
which part of the palette the texture uses.
The decoding went by a copy taken before, so the texture came out
right the first time. But whether a decoded texture has to be made
again because its colours changed is decided from the control word,
and that now named a different part of the palette. A texture using
the upper half of the palette was not decoded again when its own
colours changed, and was when the colours of the lower half did, with
whatever its own part held at that moment.
Soul Calibur's character select shows it. The second player's portrait
uses the upper half: it kept the colours of whatever was in the
palette when it was first decoded and changed to other wrong colours
whenever the first player's portrait, in the lower half, was replaced.
Loading a save state put it right, because that empties the cache.
The bits are now left alone for paletted textures, as upstream flycast
does, and IsMipmapped() no longer takes one of them for the scan order
of a paletted texture.
This has been there since palette textures were first handled on the
GPU in 2020; it is not from the recent renderer work.
The palette's conversion to host formats, and the hashes paletted
textures are keyed by, were redone when pal_needs_update said so: a
flag set by the register writes and cleared by palette_update(), which
the emulation thread called when the game started a render.
With Threaded Rendering that is the wrong thread at the wrong time. The
frame before can still be drawing, on the frontend's thread, when the
game starts the next render, and palette_update() rewrote the very
tables that frame's texture decoding was reading. It also converted
from the live registers, which by then may hold the palette of a frame
the renderer is not on yet.
palette_update() is now called by the renderer, for each frame, before
the frame's textures are processed, and reads the copy of the registers
the frame is drawn with; TexCache.cpp reads that copy throughout, as
the renderers do. Whether the tables are stale is decided by comparing
the 4 KB of palette and the control register with what was converted
last, so there is no flag for two threads to share, and a palette
written back unchanged costs no conversion and no upload.
pal_needs_update is gone.
The live test's disc now draws its background with an 8-bit paletted
texture and, after the texture's last repaint, changes the palette
entry it uses to yellow: the yellow screen takes the texture write and
the palette write both. With the conversion taken out the screen is
black and the test fails.
Three were left in the core's own sources: the count of custom-texture
load requests in BaseTextureCacheData, the reset request flag, and an
unused <atomic> include in gles.h. They are retro_atomic like the rest;
the mirror of the texture counter in tools/threads/threads_test.cpp goes
the same way. No change in behaviour.
The texture cache write-protects the pages of video memory its textures
were read from, and a write to one of them faults. Everything that
followed sat under vramlist_lock: the fault handler on the emulation
thread took it to walk the page's lock list, mark the textures changed,
free the lock blocks and unprotect the page, and the render thread took
it for every texture it locked or deleted. That was a mutex taken
inside a signal handler and held by the other thread across mprotect
calls.
The lock lists now belong to the render thread alone. On a fault the
emulation thread unprotects the page and marks it in a cMarkSet
(core/lockfree.h): the mprotect calls and two atomic operations, with
no lock, no list and no malloc or free. The render thread collects the
marks before it looks at a texture and takes the locks off those pages
itself, which is what marks their textures changed.
Unprotecting before marking is what keeps this sound. A page the
emulation thread has opened up is always marked afterwards, so the
render thread can go on assuming that a page with locks on it is
protected: where it is not, a mark is coming that removes those locks.
The same mutex guarded vmem32's list of protected ranges, which the
emulation thread walked when it mapped video memory into the 32-bit
address space of an MMU game. That list is gone. _vmem_protect_vram()
and _vmem_unprotect_vram() keep one bit per page, and vmem32 protects
the pages of a new mapping whose bit is set.
This was the last user of cMutex, which goes too.
tools/threads/run.sh has one thread mark and another collect under
ThreadSanitizer and checks that no mark is lost. With the mark taken
out of the fault path, tools/threads/live.sh fails on its screenshot:
the screen stays on the colour the texture had before.
Measured on VF3tb with the headless harness: sustained SIGSEGVs at 1-2 per frame
(300-560 per 300-frame window), all attributed to VRAM texture write-protection
by per-cause fault counters (ram=0 bm=0 vram=all after boot). The cycle: a game
rewrites a texture every frame; each write hits the mprotect'd page -> signal ->
texture invalidated + page unprotected -> texture re-uploaded on next use ->
page re-protected -> next frame faults again, forever. Each iteration pays the
signal, the invalidation, an unconditional re-conversion/re-upload, and an
mprotect -- host-time cost invisible to emulated cycles, showing up as steady
frame-time jitter. On Windows the per-fault cost is higher still (vectored
exception dispatch).
After FREQUENT_UPDATE_COUNT (8) updates, treat a texture as dynamic: leave its
vram writable and detect changes at lookup time by content hash (XXH32 over the
same range the lock covered, clamped to VRAM like the lock was). If the game
rewrote identical content, the re-upload is skipped entirely -- cheaper in every
case than the fault path it replaces. Static textures never reach the threshold
and keep the zero-cost-until-written protection. RTT locking (ReadRTTBuffer) is
unchanged.
Measured after: fault windows drop from a constant 300-560 to 0 in steady state,
with remaining bursts only where scene changes create new textures that pay
their first 8 updates before converting. dirty stays 0 for hash-mode textures,
so cache eviction of stale entries is unaffected (they are evicted via the
normal paths when deleted or the cache is cleared).
get rid of PixerPacker templates
gl4: no need for GLSL precision qualifier in GL4
vk: Refactor base and oit renderers to avoid code dup
vk: Linear tiling is faster for small, frequently updated textures
Fix Hello Kitty crash with empty first TR polygon
Fix Test Drive Le Mans crash on invalid textures
Adjacent textures in other pages still need vram protection
wince: disable vmem32 for Marionette Company 1 & 2 and Rune Jade
Disable verify in TA parsing: fix F1 World Grand Prix for Dreamcast
need RTT to vram option:
L.O.L
Miss Moonlight
Eternal Arcadia
Yu Suzuki Game Works vol.1