Direct3D 11, 12 and 9 make a render still on the graphics card into a
texture of another shape there too (CopyRender()), at every scale, not
read back every frame: Direct3D 11 copies the render's part, drawn the
console's size first where it is finer (shrink(), which its read-back
draws with as well); Direct3D 12 draws it onto a picture of the pool's to
render to; Direct3D 9 stretches it onto a texture that can be rendered to.
A renderer that cannot leaves the texture's type as it was.
A render still on the graphics card, drawn as a texture of another shape -
fewer lines, through a stride, a part of it - is made into that texture on
the card (CopyRender(): a copy, or a blit where the render is finer), not
fetched into video memory and made from there, which waited for the card
every frame such a render was made again. Its rows have to be the render's
and all it reads inside what was rendered, in the render's format; it stays
watched by write-protection and is made again with the next render there.
Vulkan and OpenGL 3 / OpenGL ES 3 have it; the rest fetch as before. A page
a dropped lock left protected is not protected again.
gl_frames counts reads off the card (GLF_READS). The DMA disc draws the
bottom half of a render made every frame: tools/gl/gl_dma.sh holds it to 40
reads in its run, where reading it back costs 70.
A render to a texture writes video memory on the console. Here its picture
stays on the graphics card, and video memory gets it later without a write
that faults, so a texture made from that memory - another size, another
format, a part of it - went on showing what was there before: a render made
every frame and drawn as a texture of another shape froze at its first
frame. rtt_watch_add() now has every texture made from what the render
covers made again (VramRendered()), all but the one rendered to, with no
change of page protection; every renderer names that one in its watch.
The DMA test disc renders a picture every frame, red then blue, and draws
it as a texture of another size: tools/vulkan/vk_dma.sh, tools/gl/gl_dma.sh
and tools/rend/d3d_dma.sh have it blue.
The SSA optimizer's sets and maps of (register, version) are sorted
arrays in memory kept from one block to the next, in the order std::map
had them; the register allocator keeps its guest registers in a table
by register with a bit each, its free host registers in a ring and the
spilled ones in an array. A block compiles to the same code, with no
allocation in the optimizer or the allocator.
A texture locked in video memory takes its block from those unlocked
before, so a texture rendered to each frame allocates nothing, and each
page's locks are a list that counts the live ones, so whether a page is
to be protected is known without going through the list.
The framebuffer a game writes itself is read with the chip's 32-bit view
mapped inline (pvr_map32() in pvr_mem.h), not through a call for every
pixel. OpenGL keeps its texture from one frame to the next, made again only
for another size, and converts the picture straight into a pixel buffer,
mapped, on OpenGL 3 and OpenGL ES 3; without one, into memory it keeps.
The per-pixel renderer's translucent polygon words are made in their buffer
mapped, new each frame. What is kept from frame to frame for the rest -
OpenGL ES 2's 16-bit indices, the framebuffer's memory, the polygon words
where the buffer cannot be mapped - is grow_buf.h's: C89 memory that is
only ever grown, never zeroed or copied when it is.
A block copy into video memory (PVR-DMA, the channel 2 texture path) does
once, for the whole of it, what its writes did a page at a time through
faults: a picture rendered there that it replaces whole is dropped instead
of read back from the card and overwritten, one it covers in part is
fetched first, and the protected pages are opened and marked written with
one change of protection a run. A picture that settles is copied off the
card the frame before it is read, several fetched at once share one batch
and one wait, and a slot taken when all are full is the oldest one's.
Vulkan, Direct3D 11 and 12 and OpenGL 3 copy into memory of their own per
picture (Vulkan without a staging image at the console's scale, OpenGL
into pixel buffers, mapped and handed over with no copy of this side's,
its framebuffers kept). tools/threads/live_prog.c has a DMA build;
vk_dma.sh, gl_dma.sh (gl_frames.c: OpenGL on Mesa's surfaceless EGL),
d3d_dma.sh and rtt_watch_test.sh check it, in CI.
A texture watched by its hashes is hashed by 4 KB pieces of video memory.
When only some of them changed, and nothing else (palette, stride), the
regions of the image those pieces are in are made again and the rest is
left: twiddled, planar, compressed (unless the codebook changed), paletted
and mipmapped textures down to their 8x8 level, up to 16 regions, while
they cover less than half of it. Vulkan makes them on the GPU over those
groups only (vkCmdDispatchBase) from just their bytes, or copies the rows
of a planar one; converted on the CPU, Vulkan, Direct3D 11 and 12 and
OpenGL upload just them. Direct3D's and OpenGL's decoders make the texture
whole; so do textures scaled up, dumped or replaced.
tools/rend/texconv_test.sh part holds every update of textures of each
kind, written a few bytes at a time, to what a full upload makes, on the
CPU and through the kernel's text.
A texture that changes often is watched by a hash of its bytes in video
memory, and a render's parse asks for it for every polygon that uses it.
It is hashed the first time in a render, and again in that render only
after the render thread has collected a write to video memory.
tools/rend/texconv_test.sh holds every request against what a full upload
makes, with video memory written between renders and in the middle of one,
and a texture asked for 300 times in a render to one hash.
ReadFramebufferGeometry() gives the picture's size first and
ReadFramebufferTo() converts it to a pointer and row pitch: the Vulkan
and Direct3D 12 upload rings and the mapped Direct3D 11 texture take it
there, instead of from the texture cache's memory copied after. Each
texel is stored once, whole. ReadFramebuffer() stays for OpenGL,
Direct3D 9, and Vulkan when the ring has no room.
The OpenGL renderers make textures from their bytes in video memory as
the Vulkan and Direct3D 11 ones do (gles/gl_texdecoder.h), on desktop
OpenGL 3 and later:
- the bytes - texels, codebook, the palette part used - go into one
ring buffer made at the start (a share of the budget, 4 to 16 MB).
With OpenGL 4.4 or ARB_buffer_storage it stays mapped and is written
in place, a frame's pieces taken back when its fence (glFenceSync),
polled with glClientWaitSync(.., 0), says it is done; without, the
frame's bytes go in one glBufferSubData (or texture upload) when they
are flushed. Nothing waits: a texture there is no room for is
converted on the CPU, as before;
- OpenGL 4.3: the ring is read as a shader storage buffer; 32-bit
texels and palette indices are written by the compute kernels through
rgba8ui and r8ui images, 16-bit ones drawn through a framebuffer into
the level, which is GL_RGB565, GL_RGB5_A1 or GL_RGBA4 (a format that
cannot be drawn into stays with the CPU);
- OpenGL 3.x: the ring goes to an unsigned integer texture once a frame
and every level is drawn;
- a planar 16-bit texture needs no kernel: its rows go from the ring (or
straight from video memory, where there is no ring) to the texture as
GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_1_5_5_5_REV and
GL_UNSIGNED_SHORT_4_4_4_4_REV (GL_BGRA), with the row length it has
there: the PowerVR's bits as they are. OpenGL ES has 5-6-5 alone, with
rows the texture's length.
What a frame asks for is recorded at the end of ProcessFrame(), before
its draws, at binding points above the renderers' own, with what is
changed put back. The kernels are made when the renderer starts: from
their SPIR-V with OpenGL 4.6 or ARB_gl_spirv; else compiled from their
text (rend/texdecode_gl.inc), their program binaries kept in the
frontend's system directory (flycast_gl_texdecode.bin, by the driver's
vendor, renderer and version and the text) and loaded from there the next
time where the driver has program binaries. OpenGL ES and OpenGL 2
convert on the CPU, as before.
The cache's textures come from a pool of the same kind as the others'
(GLTexturePool, rend/tex_pool.h), on desktop OpenGL 3 and later: made at
the start with their storage (glTexStorage2D() with OpenGL 4.2), every
level, in the five formats and RGBA8 and RGB8 for renders to textures.
A texture keeps the name it had of its own while it has the pool's. A
render to a texture draws into the pool's, through a framebuffer and a
depth buffer kept for as long as the renderer is (made at the start, at
the size of the largest shape at the scale renders are made at), for both
OpenGL renderers. Textures given back are handed out again at once:
OpenGL orders what the GPU still has to do with them.
A planar 16-bit texture whose texels a renderer takes as they are is now
offered to it whether the GPU makes textures or not (rend/TexCache.cpp):
it is a copy, which converting it on the CPU only makes slower - on
Vulkan and Direct3D 11 too.
Before it converts a texture, Update() now works out what it would
convert it with - which bytes of video memory, which converter, the
texel size, and for each mipmap level where its texels are and how they
are laid out (PlanDecode(), in rend/texdecode.h's terms) - and offers
that to the renderer (GpuDecode()). A renderer that takes it makes the
texture itself from the bytes; the texture is locked or hashed for
changes exactly as before, palette changes and the stride register
still have it made again, and the texture's type is what the CPU's
would have been. A renderer that does not, which is every one but
Vulkan, gets the texture converted here as before.
Not offered: textures scaled up (xBRZ) or dumped, which need the texels
on this side; textures the converters have nothing for; and a twiddled
texture running past the end of video memory, which the converters read
from the memory's mirror.
A core option, GPU Texture Decoding (on by default), turns it off. It
takes effect with the next texture made: a texture is the same either
way, so none has to be made again.
The 1x1 level of a compressed, mipmapped texture is made by converting
the 2x2 block its index names into a buffer of four texels and taking
the lower right one. For a paletted texture the converter writes a block
of 4x4 texels (2x4 for 8 bits a texel), two texels a row here: ten
texels into a buffer of four, on the stack. The buffer is made room
enough for that. The texel taken is the same as before.
- A planar texture's watched and hashed size was fixed when it was made
while Update() read with the stride as it is now, and the stride is
not in the cache key: changing the stride alone left a stale texture.
The stride a texture was read with is kept and a change to it has the
texture read again; one rendered to is left alone. (Upstream has the
same issue. Not keyed on the stride: render-to-texture targets are
made with StrideSel 0 and sampled with 1.)
- When a region's opaque and translucent modifier volume lists are the
same list (Crazy Taxi, Daytona USA), the per-pixel renderers (GL4 and
Vulkan OIT) draw the opaque volumes as the translucent ones (upstream
85b8c599c; two empty lists do not count as shared).
A load copied video memory over and had every texture deleted, to be
made again from what were usually the same bytes: with run-ahead, every
texture on screen converted and uploaded again every frame.
vram_load() reads video memory from the state. A page that textures were
made from - a write-protected one - is compared with the state's: the
same, it stays as it is and so do its textures; another, it is written
as the game would have written it, which is what has those textures
made again. The other pages are copied as before. (A copy over a
protected page used to fault once a page, some 200 times a load.)
Textures that are checked by a hash of their memory each time, and the
palettes, find out for themselves as they always have.
What a game rendered to a texture and has not been fetched into video
memory yet (rtt_watch) is not in a state. When a load drops it, the
texture that shows it is made again from video memory, which is what
the machine has there: rtt_watch_forget() marks its pages written.
The custom texture loader was stopped before every load, because the
textures it was working on were about to be deleted. They are not any
more, and it is left running. (With custom textures on, every load
stopped its thread, forgot the list of files, and started over.)
A state that stops short of video memory has the textures made again,
as before.
With every frame run ahead, per frame shown - the frame, a save, the
next frame and a load - on the same machine, before this series and
with all of it:
Dead or Alive 2, a fight 127.8 + 7.8 + 13.9 + 22.5 = 172 ms
5.0 + 7.9 + 5.2 + 8.1 = 26 ms
Sega Rally 2, attract mode 66.2 + 5.4 + 26.6 + 16.7 = 115 ms
3.2 + 5.6 + 3.4 + 5.3 = 17 ms
What is left of a save and a load is one copy of the machine's memory
each, 59 megabytes on a NAOMI and 26 on a Dreamcast.
A paletted texture can be left as indices and looked up in the palette
when it is drawn. A custom texture that replaces one is colours: drawn
that way it was looked up as if it were indices. With custom textures on,
paletted textures are converted before they are drawn, as upstream ended
up doing.
Clearing the texture cache destroyed textures the custom texture loader
still had a pointer to and writes to when it is done - a change of
option or a lost context while a pack is loading. If there is such a
texture the loader is stopped first, which leaves it with none; it starts
again with the next texture that asks. Nothing is added to a frame, and
no lock.
It has not been used since "the table of pages for every host", where
vmem32_init() was made to say no; this takes out what was left for it.
- core/hw/mem/vmem32.cpp and .h, and the call to it from the fault
handler - with the working out, for every fault, of whether it was a
write and which SH4 instruction it was for, which only vmem32 asked.
- Every "if (vmem32_enabled())": in the MMU (a translation forgotten,
the TLB emptied, another address space), the block manager, the
table of pages, the two 64-bit recompilers.
- _vmem_enable_mmu(): it handed P0 and P3 of the host's layout of the
SH4's memory over to vmem32 when the MMU came on and mapped them
again when it went off. Nobody takes them now, so they are mapped
once, with the rest (the two tables are one again).
- x86-64 recompiler: a quick access to memory as the host has it
mapped is for the MMU off only, so the store of the instruction's
address ahead of it and the second "how far in is the move" are
gone. Sh4Context keeps its exception_pc, unused, so that what
follows it in the context is where it was.
- bm_IsRamPageProtected(), which vmem32 asked before it mapped a page.
What stays is that with the MMU on a page of code is protected in P1
and P2 only, not in P0 and P3 as well: nothing reads or writes those as
the host has them then. The comments that said they were vmem32's say
that instead.
Why the host's mapping was given up - the numbers - was in a comment in
vmem32.cpp; it is in mmu.h now, with the table that replaced it.
No change in what runs: the whole of headless.sh passes on x86-64, on
ARM64 and 32-bit ARM under qemu and on the Windows core under wine, and
Sega Rally 2's sound over 3600 frames on x86-64 is what it was, byte
for byte.
A texture on its way to the GPU was converted into the texture cache's
scratch memory, then copied into a staging buffer, then copied to its
image by the GPU. The staging buffer was the texture's own: a Vulkan
buffer and a piece of the heap made with the image, and kept for as long
as the texture lived - a second copy of every texture in host memory.
The texture cache now asks the renderer where to convert into
(BaseTextureCacheData::UploadMemory), and the Vulkan renderer hands it a
piece of an upload ring: a few large buffers that stay mapped, handed out
a piece per upload and taken back a frame at a time, when the frame the
piece was used in has been through the GPU - the life of the command
buffer the copy is recorded in. The texture is converted there and the
copy to the image starts from there. No copy on the CPU, no buffer per
texture, nothing kept once the image has its texels. A buffer of the ring
that has gone unused for some ten seconds is given back.
What is not converted in place still goes through the ring, with the one
copy it had: a texture that is scaled up (the scaler has its own memory)
or dumped, a custom texture, and the renderer's own small textures (fog,
palette, the overlay). A small image that is written where it is, at the
pitch it has, is as before. OpenGL is as before: it has no memory to give
and the cache converts into its own.
Mipmap levels. They are converted one after another from the smallest,
and a copy to an image has to start at a multiple of four bytes, which a
level of two-byte texels after the 1x1 one does not. The levels used to
be copied one by one into the staging buffer to line them up. They are
now converted a few bytes into the piece, which puts every level but the
1x1 one where it has to be, and that level's one texel is copied to the
start: core/rend/upload_layout.h, with tools/rend/upload_layout_test.sh
(texels of 1, 2 and 4 bytes, 1 to 12 levels: every level on a multiple of
four, inside the piece, with the bytes converted for it). The old loop
took every texture that was not 32-bit for a 16-bit one; an 8-bit one
with levels would have been read and written past its end. There is none
today - a paletted texture kept as 8 bits is never mipmapped.
Virtua Fighter 4 to frame 2700, in its attract-mode fight, on Vulkan
(RetroArch from source, Mesa's software Vulkan): 163 uploads, 9.78 MB
converted in place, 1.24 MB still copied. The picture is as it was. (The
ring's buffers were made 14 times in that run, when a buffer went back as
soon as it was unused for one round; they are kept longer since, and that
has not been counted again.)
Tested: tools/threads/headless.sh; tools/threads/live.sh on Vulkan with
the validation layer, per-pixel renderer and fullscreen toggles included,
no message from the layer; the layout test. Builds for Windows.
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