274 Commits
Author SHA1 Message Date
libretroadmin f2f7ef700a Direct3D 12: the raster shaders as DXIL where the device takes shader model 6.0
dx12_dxil.inc has every raster shader and the HDR output's as DXIL,
shader model 6.0, made by dxc and signed, each held to the raster root
signature (-verifyrootsignature); kernels_dx12.sh update-dxil makes it,
and check compares it. DX12Pipelines::Init() asks the device for its
shader model before the pipeline file is read: DXIL from 6.0 on, DXBC
below, a pipeline's shaders always of one kind. The pipeline file's sum is
of the shaders chosen.

tools/rend/d3d_dxil.sh runs the test discs on Direct3D 12 as the device
answers and with it saying 5.1 (d3d_hdr.c's D3DH_SM51): the shaders are
DXIL only where the device takes it, every pipeline is made, and the
pictures are the DXBC ones.
2026-11-02 22:59:14 +01:00
libretroadmin 60a5a324cf Direct3D 12: a polygon's blend factors that read only its colour applied by its shader
The polygon pixel shader writes a second colour, and the factors of the
TSP's instructions that read only the source - zero, one, its alpha and
one less it, and for the destination also its colour and one less it -
are applied there: the pipeline has one for the source and the second
colour for the destination (dual-source blending). DX12PsoKey::Pack()
folds the key to match, so the 64 pairs of instructions are 15 pipelines,
all of them made at the start, and the startup set is queued with the keys
from earlier runs first and the commonest pipelines before the rare ones.
On the test disc no pipeline is made in a frame any more (two or three
were, a run).

The pipeline file is version 3 and keeps a sum of the shaders' bytecode:
a library made with other shaders is started again (a pipeline kept for
other bytecode never loaded and its name could not be stored again), and
a version 2 file's keys are folded. kernels_dx12.sh update-raster makes
only dx12_dxbc.inc again; the raster and HDR DXBC are made with the
d3dcompiler_47.dll that made hdr_encode_dxbc.inc.

tools/rend/dx12_blend_fold_test.sh checks every pair of instructions, with
colours in and out of range, against the PowerVR's blend.
2026-11-02 22:43:00 +01:00
libretroadmin 1ed251bfec SPG: keep the beam from one read of SPG_STATUS to the next
A game waiting on the beam reads SPG_STATUS hundreds of times a line,
and each read found the next line due and divided twice to place the
beam. spg_beam() now keeps what it found - the line, the pending
event's end, the cycles that bound the line - and answers a read on the
same line, aimed by the same event, with a subtraction. Aiming the
event, a timing change or a state loaded starts it afresh.

The headless disc runs 3000 frames in 11.5 s, from 17.8 s.

tools/pvr/spg_beam_test.sh makes every read twice, kept and afresh,
straight and with every frame run ahead, and fails where they differ.
2026-11-02 18:20:12 +01:00
libretroadmin b60d97627a Direct3D 12: draw the frame from where it is parsed
Where the adapter's memory is the processor's, kept coherent with its
cache (CacheCoherentUMA), the frame's vertices, indices and modifier
volumes' triangles are parsed straight into a buffer of a custom heap
with write-back pages, mapped for good, in four slices
(dx12/parse_arena_d3d12.h, C89): no copy into the upload ring. The
sorted passes' indices go after the frame's there. A slice is taken when
the context's fence has passed the value of the submission that last
drew from it, polled; with none free, or another adapter, the frame is
copied as ever. Render() is the base's, final, around RenderFrame().

d3d_hdr.c's D3DH_NOT_UMA has the adapter say otherwise and D3DH_EVERY
prints every frame's sum; d3d_parse_arena.sh checks every render drawn
from where it was parsed and every frame the one of the lists copied,
under wine.
2026-11-02 17:04:49 +01:00
libretroadmin bc87a9a59d Vulkan, OpenGL: modifier volumes and sorted indices from where they are parsed
The parse's arena has a part for each list the card draws from: the
vertices, the indices (with the sorted passes' room after them), and the
modifier volumes' triangles and those cut (rend/modvol_clip.h); a frame's
lists are moved into a slice together (parse_arena_attach()). The volumes'
triangles a frame draws are drawn from there with Vulkan and OpenGL, not
copied into the main buffer or uploaded each frame. With OpenGL the
per-triangle sorted passes' indices are written after the frame's, in its
index buffer, not uploaded to a buffer of their own; the per-pixel
renderer's volume array is set again when its buffer changes.

The heavy disc has a shadow volume a billion pixels off the screen,
first in its list, over a polygon that takes shadows: the volumes' cut
triangles, and those after it, drawn from their part (vk_parse_arena.sh,
gl_parse_arena.sh), which also take the disc with volumes of each cull
mode now.
2026-11-02 16:01:46 +01:00
libretroadmin 5035a1e711 CI: the AArch64 compiler's C library for ta_index_test's NEON lane
The headless job installs gcc-aarch64-linux-gnu without what it
recommends, so libc6-dev-arm64-cross, which has the C library's headers,
is installed by name. ta_index_test.sh builds and runs a program with
those headers under qemu before it takes the NEON lane; without them it
says so and goes on, and with NEON=required, as the job runs it, fails
saying why.
2026-11-02 15:11:54 +01:00
libretroadmin 892c062a0c OpenGL: draw the frame from where it is parsed
With ARB_buffer_storage (OpenGL 4.4) the frame's vertices and indices are
parsed straight into a vertex and an index buffer the card reads, of
immutable storage mapped persistently in the client's memory, one pair a
frame in flight (gles/parse_arena_gl.h, C89): no glBufferData() copy of
them each frame. A slot is taken when the fence after the last frame
drawn from it has passed, asked without waiting; with none free, or where
the mapping reads back slowly, the frame is copied as ever. Both renderers
draw their strips from the frame's buffers and their own quads (VMU
screens, crosshairs, framebuffer) through their own. parse_arena.h's
slices can be bound apart, a buffer each.

A desktop build on an OpenGL ES context clears depth with glClearDepthf():
glClearDepth() is not ES's, and left the clear value as it was.
gl_parse_arena.sh checks every frame against the copy (GLF_NO_STORAGE),
no OpenGL error under MESA_DEBUG, and OpenGL ES as ever; gl_frames counts
no persistent mapping as a read.
2026-11-02 14:57:00 +01:00
libretroadmin b089e507fd TA parse: strips the vertex loop finds drawable need no second look
The fast vertex loop tests each vertex it converts for being left out
of the indices - three words at once with SSE2 or NEON, as it has them
in hand - and marks a strip it wrote whole with none such in a bit a
polygon (ta_strip_ok, ta_index.h). make_index() then reads none of that
strip's vertices again. Any other strip is looked at as before. The
parse is 21-26% faster on the heavy disc (2.17 to 1.60 ns a vertex
with no vertex left out), its output the same on every test disc.

The heavy disc has three strips a list with a vertex left out (an
infinite x, a NaN depth, a huge y). ta_parse_bench.sh parses each frame
once more with no strip known good and fails if anything differs;
ta_index_test.sh tests the many-at-once test against the reference, as
SSE2, scalar and NEON (under qemu where there is an AArch64 compiler).
2026-11-02 14:22:34 +01:00
libretroadmin 9d123ad27b vk_hdr: give back what a read of the picture made
Each read back of an image (the picture, the upscaler's motion vectors
and depth) makes a buffer, its memory and a command pool: they are
destroyed after it, on every path. The buffer left over after the
core's teardown was this frontend's, not the core's.

vk_parse_arena.sh's change of size now asks for no buffer left at all,
in place and copied, rather than as many as with the lists copied.
2026-11-02 13:47:34 +01:00
libretroadmin b48265ebd6 TA parse: C89 lists and strip indices; Vulkan draws from where it parses
The frame's lists are ta_list.c's (C89): List<T> is a typed facade over
it. A strip's indices are ta_index.c's C89 kernel: one branch-free check
of the whole strip, then a straight run of indices; the per-vertex loop
only where a vertex is bad or the room runs out. The parse is about 20%
faster (2.74 to 2.17 ns a vertex on the heavy disc), its output the same
byte for byte.

With Vulkan on a machine whose video memory the CPU reads back fast (one
memory, as lavapipe's and integrated GPUs'), the vertices and indices
are parsed straight into a slice of memory the card reads, one slice a
frame in flight (rend/parse_arena.h, vulkan/parse_arena_vk.h), and the
translucent sort writes after them: no copy into the main buffer. Other
machines copy as ever.

Tests: tools/pvr/ta_index_test.sh (against the old loop, 1M strips),
tools/rend/parse_arena_test.sh, tools/vulkan/vk_parse_arena.sh (every
render in place, pictures those of the copy, each renderer, threaded,
upscaler, a change of size), tools/pvr/ta_parse_bench.sh; vk_hdr gains
VKH_UNCACHED, VKH_OPTS_AT and VKH_BUFFERS.
2026-11-02 13:11:59 +01:00
libretroadmin babc17aced Render to texture: views through a narrower stride made on the card too
A texture read through a stride narrower than it - a part of a render
drawn through TEXT_CONTROL - is made from the render on the card by every
renderer now, not only Vulkan: the stride's columns copied and the rest of
each row cleared, to what zeros are in the texture's format (ClearOpaque():
opaque black in 565, as the read-back has it; Vulkan's was clear).

vk_hdr counts what the core reads back off the card (VKH_READS), and
tools/vulkan/vk_dma.sh holds the DMA disc to 40 reads as gl_dma.sh does.
The disc's view of a render made every frame is a stride texture now: blue
up to the stride and black past it, the same as the read-back's picture,
alpha included.
2026-11-02 02:32:07 +01:00
libretroadmin 78d2c77623 Render to texture: a texture of another shape made from the render on the card
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.
2026-11-02 01:27:40 +01:00
libretroadmin 3673ad27a9 Render to texture: textures made from what a render covers are made again
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.
2026-11-02 00:54:05 +01:00
libretroadmin a68605c46b OpenGL: render-to-texture copies build on macOS and the Switch
glsm's glFenceSync returns a void *, which clang does not make a GLsync
without a cast, and glsm has no glFlush: the copies off the card are now
flushed by a wait of no time on the last one's fence, with
GL_SYNC_FLUSH_COMMANDS_BIT.

tools/gl/gl_shapes.sh compiles the OpenGL renderers on Linux as macOS
(clang) and the Switch (libnx's GL symbols) build them, in CI.
2026-11-01 22:09:01 +01:00
LibretroAdmin a9e6d33590 naomi: the network DIMM board of GD-ROM games, at firmware 3.17
GD-ROM sets that come with the network DIMM board now have it answer, as
firmware 3.17 with its network settings, instead of seeing no board at
all: games that check the board's version at start-up (error 14 below
3.17) boot. The network commands are still refused, and those games go on
after their network check times out. What the plain and the network board
leave in the NAOMI's memory is in naomi_dimm.c, C89, with a regression
lane in tools/dimm. The network board's part of the save state is a new
V31 part; states from before it load with the board silent until reset.
2026-11-01 21:04:39 +01:00
libretroadmin a864367e52 States: no copy from a chip the machine lacks; run-ahead tested
A state saved or loaded copied the Dreamcast's SRAM, which it does not
have: no memory and a size of 0, a copy from or to a null pointer
(UBSan). Parts of no size are now nothing to copy.

headless.sh's run-ahead stage passes: its do-nothing OpenGL read back
from the mapping the core writes through, so what it fetched off the
card depended on the core's history, and frames run from a state loaded
were not the frames run straight. Readbacks are now the same bytes every
time. headless.sh runs in CI (Headless).

gl_frames runs frames ahead (GLF_AHEAD), and tools/gl/gl_ahead.sh runs a
disc (live_disc.py --keep) that renders to a texture at the very end of
a frame and only draws with it after: every frame run ahead has to show
what a plain run shows, the picture saved in the state while still on
the card. On OpenGL and OpenGL ES, and under ASan and UBSan in CI.
2026-11-01 20:56:40 +01:00
libretroadmin 68a575a60d Recompiler: every block let go when the core is
A block holds the blocks that jump to it, so two that jump to each other
hold each other. At the end the block tables were left as they were, and
when they went with the core's statics - the core unloaded with
dlclose(), or the process ending - those blocks and what they held were
never freed: about half a megabyte for a few hundred frames of the test
disc, each time a frontend unloads the core. bm_Term() now cuts the
links between them and empties the tables, nothing running the code by
then.

tools/threads/unload_leaks.sh runs the test discs' frames with the core
built with AddressSanitizer, unloads it, ends the process and fails on
anything LeakSanitizer finds; the sanitizers workflow runs it.
gl_frames.c unloads the core when no frame was drawn too.
2026-11-01 20:17:57 +01:00
LibretroAdmin 37b027695e Vulkan: the screen's picture is handed over whole, not by region
The raster and per-pixel renderers' render passes made the frontend's reading
of their picture wait for them by region. The frontend reads it from a render
pass of its own, or by a copy, and anywhere in it: a GPU that draws in tiles
let it read tiles whose translucent polygons were not drawn yet, a lattice of
blocks with only the opaque ones. The dependency is now on the whole picture,
for a shader, a compute upscaler or a copy reading it.

tools/rend/speckle_test.sh checks that no dependency out to what reads a
render pass's picture is by region.
2026-11-01 19:52:59 +01:00
libretroadmin 022b9c3d1f OpenGL: a polygon drawn outside its clipping rectangle is, and its state set only when it changes
The per-triangle and per-strip renderer's program discards what is
inside the clipping rectangle when the polygon asks to be drawn outside
it, as the per-pixel one's does: it compared the flag with -1, which it
never is, and drew the whole polygon. tools/gl/gl_clip.sh draws three
such polygons of one program, the middle one's rectangle another, on
each renderer with OpenGL and OpenGL ES (the test disc built with
-DLIVE_CLIP: tools/threads/live_disc.py --clip), and is run in CI.

That renderer sets a polygon's trilinear blend, palette and clipping
uniforms only when they differ from what its program was last given,
and its textures' LOD bias and anisotropy only when they differ from
what the texture has. The texture parameters are kept by texture name
in a table, not a map, and every texture deleted goes through the cache.
2026-11-01 19:43:47 +01:00
LibretroAdmin 03618ee948 Vulkan raster renderer: the screen's picture is not cleared while the frontend reads it
The screen's render pass clears and draws into an image the frontend was
handed before, on the queue the two share, and now depends on whatever read
it there - a shader, a compute upscaler, a copy - being done first, as the
per-pixel, SSAA and render to texture passes already do. A GPU that draws in
tiles showed tiles of the next frame with only its opaque polygons drawn, the
translucent ones over them missing, per triangle and per strip alike.

tools/rend/speckle_test.sh checks the dependency in the sources.
2026-11-01 19:40:20 +01:00
LibretroAdmin 6df4ce02e8 Vulkan per-pixel renderer: a frame slot's count goes to the bands of its kind
What a frame slot counted is read by whichever drawer comes to the slot next
and goes to the bands of the kind of frame drawn there, the screen's or a
render to a texture's, kept apart in OITBuffers (rend/oit_bands.h). The two
take turns at the slots, so a screen whose translucent fragments overflow the
buffer is drawn in bands with any number of renders to a texture a frame and
any count of slots, as the other per-pixel renderers do.

tools/rend/oit_bands_test.sh checks it over every count of slots and of
renders to a texture a frame.
2026-11-01 19:33:18 +01:00
libretroadmin f288b2ab86 Translucent sorter: C lists and a radix sort, the triangles sorted where the card reads them
The sorter keeps its lists in memory only ever grown (grow_buf.h): a
SortList of draws a render pass, no std::vector anywhere in it or in the
renderers that call it. Triangles are sorted by depth with a stable
radix sort of (key, index) pairs (rend/sort_pairs.h, C89: three passes of
11 bits, a pass of one digit skipped; merged runs below 512, insertion
below 64), the same order a stable sort by < gives - about 8 times
std::stable_sort at 20,000 triangles. The strips (per-strip sorting) and
the prepass's runs go the same way, by the radix sort and a C merge sort.

SortPassesTo() sorts every pass and its prepass into one index array, and
SortPassesRoom() says how large it can be: Vulkan, Direct3D 11 and 12 call
these instead of each having the loop. Direct3D 9 now sorts straight into
its locked index buffer, as the others do into their mapped ones, instead
of into a vector per pass copied in after. OpenGL sorts into one buffer
and narrows it to 16 bits in place where the indices are.

tools/rend/sort_pairs_test.sh checks the sort against a stable sort by <
(CI). tools/rend/d3d9_frames.c is a Direct3D 9 frontend that checksums
every frame, and tools/rend/d3d_same.sh compares two Windows builds with
it and with d3d_hdr (Direct3D 9 needs Microsoft's d3dcompiler_47 under
wine: D3DCOMPILER).
2026-11-01 17:23:27 +01:00
libretroadmin 2c40409532 Vulkan: what is the same from one frame to the next is not made again
A polygon's texture descriptor set is written once and kept, a frame slot's
for as long as its frames bind it, known by the serials of the views it
holds (a view's serial is never another's, where a handle can be made
again at the same value), and handed to the draw that binds it with no
second lookup: plain arrays, an open-addressed table and a stack of free
sets. A render to a texture keeps its framebuffer while its attachments
are the same ones, the command pools keep the memory their commands were
recorded in and submit from a list kept for it, and the descriptor writes
and the main buffer's pieces of a frame are arrays on the stack.

The temporal upscaler writes where each vertex was straight into the
memory the GPU reads it from (the Vulkan buffer mapped, Direct3D 12's
upload ring), and matches strips through grow_buf.h memory and tables kept
from frame to frame - the kept strips found by signature through a
counting sort's index - instead of vectors and maps made again each frame.
2026-11-01 16:34:05 +01:00
libretroadmin c9ff163eeb Render to texture: a DMA over one is not fetched first, and the copies off the card are begun ahead
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.
2026-11-01 16:33:58 +01:00
LibretroAdmin 1bdca17e0e tools: a test disc that writes into its textures a few bytes at a time
live_disc.py --atlas draws textures of every layout texel for texel and
writes a little of each every frame. tools/vulkan/vk_parts.sh runs it on
each renderer, with GPU decode and threaded rendering on and off: the
pictures have to be the same, and the same as another core's when given.
2026-10-28 16:41:24 +01:00
LibretroAdmin 8245523a55 Texture cache: make again only the parts of a texture that changed
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.
2026-10-28 16:41:20 +01:00
LibretroAdmin 88f558a2cf Texture cache: hash a texture watched by its hash once a render
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.
2026-10-28 13:24:15 +01:00
LibretroAdmin c322e8905d tools: check the cull mode set with the draws, and without the extension
live_disc.py --cull adds modifier volumes and translucent polygons of
each cull mode. vk_hdr counts pipelines by cull mode and the cull modes
set, and can hide a device extension (VKH_HIDE_EXT). vk_pipelines.sh
runs on that disc, and each renderer again without
VK_EXT_extended_dynamic_state: no pipeline with the cull mode dynamic
then, and the same picture.
2026-10-28 11:06:43 +01:00
LibretroAdmin ac14bc18bf tools/vulkan: check no pipeline is made on the frontend's thread
vk_hdr counts the pipelines made on each thread (VKH_COUNT_PIPELINES),
can wait before its first frame (VKH_LOAD_PAUSE_MS) and unload the game
so the core saves its pipeline cache (VKH_UNLOAD). vk_pipelines.sh runs
each renderer without the cache file, with it, and with its driver data
spoiled: none made on the frontend's thread while the context is made,
the keys kept and taken again, the pictures the same.
2026-10-28 08:23:04 +01:00
LibretroAdmin 4538f867d0 dx11: write the sorted indices into a mapped buffer, copied on the GPU 2026-10-28 08:23:04 +01:00
LibretroAdmin 1e0e7bf304 dx12: make the start's and SSAA's pipelines ahead on a thread 2026-10-28 08:23:04 +01:00
LibretroAdmin b63e02de44 dx11: keep compiled shaders on disk and make the pixel shaders ahead on a thread 2026-10-28 08:23:04 +01:00
LibretroAdmin 2f2aa189db rend: a thread to make Direct3D shaders and pipelines ahead, and a file of compiled shaders 2026-10-28 08:23:04 +01:00
LibretroAdmin 1a678828f1 NAOMI: spread interleaved ROMs into the cartridge as they are decoded
archive_entry_stream() hands a member over in pieces: where it lies when
it is addressable, else a deflated or stored zip member decoded 64 KiB
at a time and checked against its CRC as it goes. The ROMs a cartridge
or BIOS shares a word at a time are spread straight from those pieces,
and a BIOS ROM is decoded straight into the BIOS. The word spreader
moves to naomi_interleave.c (C89), which the archive test now drives.
2026-10-27 15:58:53 +01:00
LibretroAdmin 5ce2ba0443 libretro-common: sync from RetroArch
The Win32 read-ahead over a mapped view now runs on a thread of the
file's own, also from Windows 8 on, so a disc read never waits on
PrefetchVirtualMemory. The 7z reader takes bzip2 folders through the
new encoding_rbzip2.c, added to every list that builds the 7z reader.
The Vulkan symbol wrapper stays the core's own.
2026-10-27 15:37:52 +01:00
libretroadmin f62d495d2d CHD: decode ahead per stream, so CD audio and data do not collide
A CHD was read through one decoded hunk and a thread that decoded one
hunk ahead. A game that plays CD audio while it streams data reads the
two from different places a sector at a time, so each threw out the
other's hunk and aimed the thread away from it, and the hunks were
decoded on the emulation thread: a stutter a GDI never shows.

Each stream (data tracks, audio tracks) now reads through its own
libretro-common vfs_prefetch, whose thread decodes the hunks ahead on an
image handle of its own, one window per hunk; sectors are lent from the
windows in place, and a read command's Prefetch() sets the data
stream's thread going before the first sector is asked for. The core
defines HAVE_THREADS where it builds threads, so the prefetcher is not
compiled to stubs. tools/chd/chd_streams_test, run from tools/chd/run.sh
and now in CI, reads audio and data at once and fails when the reading
thread does the decoding.
2026-10-26 14:49:16 +01:00
libretroadmin b9609bd95f Direct3D 12: the renderers draw for the frontend's temporal upscaler
The per-triangle, per-pixel and compute renderers on Direct3D 12 draw
for the frontend's temporal upscaler (rend/upscaler.h) as the Vulkan
ones do: while it has the frames, each frame of the screen is drawn at
the size it asks for, under its jitter, and ends with the motion vector
pass over the depth the frame left - the opaque and punch-through
strips again, each vertex with where it was last frame in a second
stream - into a picture of this sync index's, with the depth drawn
whole into another of its own; both are handed over with the picture.
The compute renderer's walks of their own store each pixel's depth
(PVRC_DEPTH, as Vulkan's), which the pass's pixel shader then tests
against, no depth buffer being bound. SSAA is off while the upscaler is
on, which is the anti-aliasing then.

The shaders are DXBC and DXIL made from the sources; the upscaler's
walks have a file of their own, checked by the bindings test. The
Direct3D 12 harness (tools/hdr/d3d_hdr.sh) has a temporal upscaler of
its own now: it asks each renderer for frames of 960x720 and reads the
motion vectors and the depth back to look at them, as the Vulkan harness
does.
2026-10-26 01:39:12 +01:00
libretroadmin e675ea22fe Vulkan: the compute renderer draws for the temporal upscaler too
The compute renderer's frames go to the frontend's temporal upscaler as
the raster renderers' do: drawn at the size it asks for under its
jitter (the frame's transform in pixels moved with it), the screen's
walk storing each pixel's depth in an image of the frame slot's
(pvr_compute.h, PVRC_DEPTH_ON, as the raster renderers' depth buffer
has it), and the motion vector pass drawn over that depth in the
frame's command buffer, from the frame's vertices and indices in its
words. That depth is no attachment: the pass samples it and tests
against it in its fragment shader (shaders/mv_depth.frag), with a
layout of its own.

The depth blocks are Vulkan's: the Direct3D 12 kernels' text is
without them, and their bytecode is as it was.

tools/vulkan/vk_hdr.sh and vk_sanitize.sh run the compute renderer
with the upscaler; vk_hdr takes a depth stored as a float.
2026-10-25 16:03:28 +01:00
libretroadmin fab5ee678c Vulkan: the per-pixel renderer draws for the temporal upscaler too
The per-pixel (OIT) renderer renders at the size the frontend's
temporal upscaler asks for, under its jitter, keeps each frame's depth
and draws its motion vectors over it, as the per-triangle renderer
does. Its depth is one image a frame slot while the upscaler has the
frames, stored by the last render pass instead of dropped, with the
render passes' framebuffers made for it; SSAA is off then, as the
upscaler is the anti-aliasing and reads a single-sample depth.

The motion vector pass is one piece of code for both renderers
(rend/vulkan/motion_pass.cpp): the pass, its pipelines by cull mode,
the vectors' images and the strips' previous positions.

While the upscaler is active the Internal Resolution and SSAA core
options have no effect, and are hidden.

tools/vulkan/vk_hdr.sh and vk_sanitize.sh run the per-pixel renderer
with the upscaler, the inputs read back and checked under the
validation layer and the sanitizers.
2026-10-25 15:37:29 +01:00
libretroadmin d791f07107 Vulkan: frames for the frontend's temporal upscaler (DLSS)
The per-triangle Vulkan renderer draws for RetroArch's temporal upscaler
(RETRO_ENVIRONMENT_GET_HW_RENDER_UPSCALER_INTERFACE): it renders at the
size the upscaler asks for, under a Halton jitter, keeps each frame's
depth and draws its motion vectors, and hands both over with the
picture. The frontend then upscales to its viewport.

The Dreamcast sends screen-space vertices and no camera, so motion
vectors cannot come from a matrix. rend/upscaler.cpp matches each
opaque and punch-through strip of a frame with the strip of the frame
before that was sent under the same polygon header with the same number
of vertices, in order; the motion vector pass (shaders/mv.vert) draws
the difference over the frame's depth, equal or nearer, in pixels of
the render. A strip with no match, or one that jumped, stands still.

While the upscaler has the frames, the TA is parsed on the CPU (the
match needs its strips) and SSAA is off (the upscaler is the
anti-aliasing, and reads a single-sample depth). The other renderers
draw as ever, and the upscaler is told nothing.

tools/rend/upscaler_test.sh pins the matching, the request and the
inputs; tools/vulkan/vk_hdr.c gains a frontend with an upscaler
(VKH_UPSCALER), which vk_hdr.sh and vk_sanitize.sh run: the core draws
at the size asked for and hands over motion vectors and depth of that
size, read back and checked, under the validation layer and the
sanitizers.
2026-10-25 12:33:53 +01:00
libretroadmin e7976030dd HDR: Direct3D 12 compute renderer's exact SSAA and X-scaler averages
With the HDR output, the screen's walk of Direct3D 12's compute renderer
also stores each pixel's channel sums, as Vulkan's does. It does so with
SSAA and with the X scaler's pairs of pixels. The sums are written into
the per-pixel renderer's R16G16B16A16_UINT image, so the encode
(hdr_encode.hlsl's HDR_ENCODE_SUMS) takes the exact average of the
samples where the picture is their resolve, and does not dither. The
walk is pvr_compute.h's with PVRC_SUMS: sumsImage at u1 of space 2,
which a second view in each piece's table holds, a null view for the
other kernels. Its variants for 1, 2, 4 and 8 samples, sorted and
unsorted, are DXIL of their own in dx12_compute_hdr_dxil.inc, made with
kernels_dx12.sh update-compute-hdr. Their pipelines are made at Init()
only with the HDR output on, so at 24-bit none of this exists. The sums
count where the walk alone wrote the picture, as on Vulkan: no Y filter
over it. Renders to a texture keep their walks without the sums.

dx12_compute_dxil.inc is unchanged: its sources still leave the
PVRC_SUMS blocks out. The new kernels are made by dxc 1.9.2609 with the
open-source validator (libdxil.so), which signs them. Each one is also
held to the compute renderer's root signature (-verifyrootsignature);
every kernel in both incs fits it. tools/rend/dxil_bindings.c
(kernels_dx12.sh bindings, in CI) reads the bindings each kernel
declares from its DXIL's PSV0 part. It checks that every HDR walk is
signed, binds what its walk binds plus the sums as a typed UAV of a 2D
texture, and that no other kernel binds the sums. It needs neither
Direct3D 12 nor dxc. Wine's vkd3d cannot make the compute renderer's
DXIL kernels, so under wine its frames are the per-pixel renderer's
(d3d_hdr.sh says so). Pipelines are now also released when frames on
the GPU are done with them, the walk's unsorted variants included.
2026-10-25 09:05:43 +01:00
libretroadmin a15b706fa4 HDR: Direct3D 12 per-pixel SSAA exact; Direct3D 12 core judged
With the HDR output and SSAA, Direct3D 12's per-pixel renderer now
encodes the exact average of the samples, as Vulkan's does, rather than
the byte the picture rounded it to, and does not dither. The screen's
last resolve also stores each pixel's channel sums. It uses a pipeline
of its own (DX12_RATE_PIXEL_SUMS), with the OIT_FINAL shader built with
DX12_OIT_SUMS, writing into an R16G16B16A16_UINT image. The sums are
scaled to HDR_SUMS_SAMPLES (16), now in hdr_output.h for every backend.
The image is made the first time, grown only, and is bound at u4 by a
root table of its own (DX12Context::OitSumsSlot). The encode reads the
sums (hdr_encode.hlsl's HDR_ENCODE_SUMS, PS_SUMS and PS_SUMS_LINEAR for
the post effects) where the picture is their resolve by
pvr_resolve_join(). Where something was drawn over it, the encode reads
the picture as before. Renders to a texture store no sums, and frames
with the write-out are encoded from the picture.

The new shader is built into an inc of its own, dx12_hdr_dxbc.inc,
with kernels_dx12.sh update-hdr. kernels_dx12.sh leaves the
DX12_OIT_SUMS block out of the other shaders' source, so
dx12_dxbc.inc and its sums are unchanged. At 24-bit none of this runs.

tools/hdr/hdr_d3d.sh 12sums gives the encoder the sums as the resolve
stores them, with an overdrawn strip, and holds the encode, then the
post effects' (bloom and aberration), to the exact average's. The old
code fails it (up to 10.7 times the tolerance, dithered).
tools/hdr/d3d_hdr.sh now also runs the Direct3D 12 core on wine's own
vkd3d with VKD3D_CONFIG=virtual_heaps: 24-bit, HDR10, scRGB and the
post effects, on every renderer. Without virtual_heaps the root
signature takes more than lavapipe's eight descriptor sets. It runs no
SSAA there: wine's vkd3d has no eval_snapped. CI runs
kernels_dx12.sh check.
2026-10-25 08:26:01 +01:00
libretroadmin 9140c5f71a HDR: post effects - bloom, halation, chromatic aberration
The core options of beetle-psx's HDR post effects: HDR Bloom (and its
Selection, Range and Convolution), HDR Halation, HDR Chromatic Aberration
and HDR Post: Compact Intermediates. They are shown with an HDR Color
Format, on Vulkan, Direct3D 11 and Direct3D 12, every renderer of each.
With them on, the encode is three steps on every backend. First the
picture, or SSAA's channel sums, goes into scene-linear light in a
half-float image of its size (or 11/11/10 with Compact Intermediates,
where the device draws, blends and filters it). Then the bloom is built
from that image by the passes of one plan, hdr_post_plan_make(): the
bright pass, then either the two-band blur or the six-octave pyramid.
Last, the composite adds the aberration and the tinted bloom and encodes
as before. The shaders are beetle-psx's, in GLSL and in HLSL (one DXBC
for both Direct3D backends). What they need is made the first time they
are on, and the rasterizers' fused HDR subpass is left undrawn while
they are. Off, none of it runs, and nothing of it exists.

tools/hdr/hdr_post_ref.c runs the same three steps on the CPU from the
24-bit picture, rounding each image to its storage format and tapping
where the shaders tap, and reads the options the way the core reads
them. vk_hdr --post holds the scRGB picture to it:
 - tools/vulkan/vk_hdr.sh runs every Vulkan renderer, with and without
   SSAA, and each setting, and checks that other settings than the
   picture's fail;
 - tools/hdr/d3d_hdr.sh runs the Direct3D core;
 - tools/hdr/hdr_d3d.sh post11 and post12 run the Direct3D 11 and 12
   encoders under wine.
vk_sanitize.sh runs them under ASan and UBSan.
2026-10-25 07:06:04 +01:00
libretroadmin e7cde6e176 HDR: ACES 2.0's tables off the frame; SSAA and the X scaler exact
ACES 2.0's tables (the JMh parameters, the tone scale's LUT and the gamut
scales) were made on the frame that changed the peak, the gamut or the
surround. They are now made by the HDR worker, like the cube, through
the same lock-free hand-over: a slot for each kind of job taken by
atomic exchange, a CAS-pushed list of what is done, a stop flag for each
job, the worker parked on an eventcount. Nothing waits for it. Until the
new tables are done the encode keeps the old ones, and their cube, with
the gamut they were made for; before any, the tone scale alone
(HDR_DT_ACES2). A fill takes back what the worker finished before it
reads the tables, so a block is never half of the old tables and half of
the new ones. ACES2_InitSurround() is the reentrant init the worker
calls. hdr_dt_finish() builds in place, for the tests. Without threads
(TARGET_NO_THREADS) the build is done in place as before.

With the write-out off, what SSAA resolved is now encoded from the exact
average of its samples, not from the byte the picture rounded it to, and
is not dithered. Where something was drawn over the picture after the
resolve (the VMUs, the crosshairs), the picture is encoded as before:
the encode checks that the picture is the samples' resolve by the shared
rule (pvr_resolve_join()).
 - Vulkan per-triangle and per-strip: the HDR subpass reads the samples
   as a second input attachment.
 - Vulkan per-pixel: the last pass also stores each pixel's channel sums
   (OIT_FINAL_FRAG_S1_SUMS, an R16G16B16A16_UINT image in the OIT
   buffers' set), which the encode samples.
 - Vulkan compute: the walk stores the sums too, under a specialisation
   constant, which is 0 at 24-bit. It does the same for the X scaler's
   pairs of pixels, at one sample a pixel as well. Renders to a texture
   have walks of their own that store none.
 - Direct3D 12 per-triangle: the encode reads the multisampled target
   (PS_SSAA, hdr_encode_dxbc.inc).
The sums are scaled to 16 samples, so one encode pipeline takes any
count. Direct3D 12's compute kernels are unchanged: kernels_dx12.sh
leaves the PVRC_SUMS blocks out of their source, so their DXIL and sums
are as they were. At 24-bit none of this runs.

tools/vulkan/vk_hdr.sh gains --exact cases on two new discs built from
live_prog.c (live_disc.py --slant and --hscale). The first has a slanted
shaded triangle and is run on every renderer at 4x. The second adds the
X scaler into a 32-bit framebuffer and runs the compute renderer at one
sample a pixel. The old code fails both: it dithers, at most 1.08 of a
step off. tools/hdr/hdr_d3d.sh 12ssaa checks Direct3D 12's encode with
samples, with an overdrawn strip. tools/hdr/hdr_cube.sh checks the
tables: the first fill and a gamut change keep what there is, and both
converge on the tables made in place, and no block between is
a mix of two sets of tables. vk_sanitize.sh adds the per-pixel
and compute renderers with SSAA at scRGB.
2026-10-25 00:13:22 +02:00
libretroadmin 192c743256 Undefined behaviour the sanitizers find, DEBUG_ASAN, a refused device
Each is fixed where it is, and the result is the same as before where
the old code was defined: the flash ROM's CRC (flashrom.h) and the AICA
filter's fraction (sgc_if.cpp) no longer shift negative values left;
div1 subtracts b modulo 2^32 rather than negating INT_MIN; the x86 and
x64 recompilers keep their host registers in an s8 and the ARM ones
have a -1 enumerator, so the lists' -1 end is a value the type has; the
modifier volumes' bounds read a vertex's x and y with an unaligned 8-byte
load (rend/shadows.h). xbrz's debugger break is MSVC's own, and is now
only there: make DEBUG_ASAN=1 and DEBUG_UBSAN=1 build with gcc and clang.
A renderer that cannot be set up on the frontend's context (Direct3D 12
or 11 with no device or queue) no longer takes the frontend down: the
frames run, none is drawn, and the next context reset tries again.

The Sanitizers workflow builds the core with each sanitizer and runs the
test disc on every Vulkan renderer at 24-bit and HDR, failing on any
report (tools/vulkan/vk_sanitize.sh); tools/hdr/d3d_hdr.sh runs the
Windows build on a refused Direct3D 11 and 12 device, threaded rendering
on and off.
2026-10-24 22:45:27 +02:00
libretroadmin 0d947c9797 HDR output: Color Format (24-bit, HDR10, scRGB) and Display Transform
A Color Format core option hands the frontend 30-bit HDR10 or 64-bit
scRGB instead of the 24-bit picture, on Vulkan, Direct3D 11 and Direct3D
12 and every renderer of each, through the libretro HDR API (the pixel
formats, GET_DISPLAY_METADATA, GET_HDR_METADATA). The picture is placed at
the frontend's paper white and mapped onto the display through a Display
Transform (beetle-psx's, vendored in rend/hdr/: Reinhard knee by default,
which keeps everything below paper white as it was, ACES, ACES 2.0, AgX,
PBR Neutral, GT, Hable and others), with optional HDR Expansion. At
24-bit, or when the frontend refuses, none of it runs and the picture is
the same to the byte. On Vulkan the encode is the last subpass of the
frame's render pass on the rasterizers and one draw on the compute
renderer and CPU framebuffers, recorded into the frame's own submission
(after the chip's write-out on a frame that has one);
on Direct3D 11 and 12 it is one draw in the frame's context or command
list, from one shared DXBC blob (tools/rend/kernels_dxbc.sh update-hdr).
Nothing is copied and no submission or wait is added. ACES 2.0 (Full)'s
cube is baked off the frame by a worker thread handed its jobs through
atomics and an eventcount, without locks.

tools/vulkan/vk_hdr.sh runs every Vulkan renderer at each Color Format
and judges the pictures; tools/hdr/d3d_hdr.sh does the same with the
Windows build on Direct3D 11 under wine; tools/vulkan/hdr_hlsl.sh and
tools/hdr/hdr_d3d.sh hold the HLSL, and the Direct3D 11 and 12 encoders
under wine, to the GLSL over every curve and setting; tools/hdr/hdr_cube.sh
holds the worker's cube to the cube baked in place, under TSan and
ASan/UBSan. All of them run in the Vulkan negotiation workflow.
2026-10-24 22:45:23 +02:00
LibretroAdmin ca3fee8eaa rend: the chip's write-out on the raster renderers, with the 16-bit dither; the old PowerVR2 filter goes
The core option is now "PowerVR2 scaler, flicker filter and dither"
(reicast_pvr2_scaler, still off by default). It does what the
PowerVR2 does to the picture as it writes the tiles to video memory,
in the chip's order: the X scaler, the Y filter, then the packing to a
framebuffer of 16 bits a pixel and its dither (FB_W_CTRL). The picture
shown is that framebuffer as the video output reads it back, with
FB_R_CTRL's fb_concat in the low bits.

- rend/tile/pvr_scaler.h: pvr_fb_word() and pvr_fb_write(), the
  packing to 0555/1555 or 565 with rend/rtt_watch.cpp's 4x4 pattern
  (the same arithmetic as the renders to a texture). At internal
  resolution N the pattern goes by the chip's pixel (N picture pixels
  to one). 4444 and the 24/32-bit formats are left as they are.
  Checked against an independent copy of rtt_watch.cpp's packing plus
  TexCache.cpp's read-back: no difference in 195072 cases.
- The compute renderer's in-place pass (PVRC_K_YFILTER) does the
  packing and dither after the Y filter, and runs on any frame with
  either (ComputeFrame::WriteOut()). tile_ref and refsw do the same.
  refsw did not dither the picture before: it does now.
- The raster renderers run the same pass in place over their picture
  after the frame (before the VMUs and crosshairs on Direct3D and
  OpenGL; on Vulkan they are in the frame's render pass, so the pass
  runs over them too):
  - Vulkan, both renderers: the compute renderer's own SPIR-V
    (vulkan/writeout.cpp).
  - Direct3D 12, both renderers: the compute renderer's own DXIL
    (dx12/dx12_writeout.cpp). Needs typed UAV loads of R8G8B8A8_UNORM.
  - Direct3D 11, both renderers: the same pass in HLSL, compiled when
    the renderer starts (dx11/dx11_writeout.cpp). Needs typed UAV
    loads of R8G8B8A8_UNORM (TypedUAVLoadAdditionalFormats).
  - OpenGL 4.3 and later, both renderers: the same pass in GLSL,
    compiled (and cached) when the renderer starts
    (gles/gl_writeout.cpp). The frontend's framebuffer is not a
    texture of the core's, so the picture is blitted to the pass's
    texture and back.
  Not on OpenGL ES, OpenGL 3 or Direct3D 9.
  The pictures are made writable by a compute shader only when the
  option is on. Turning the option on or off counts as a change of size:
  the renderer makes its pictures again between frames, never during
  one. Off, the renderers do what they did before, at no cost.
- The rasterizers' X scaler is not done here: they still squash the
  game's double-width drawing in the transform, as before. No dumped
  game sets SCALER_CTL's horizontal scaling.

The old "PowerVR2 Post-processing Filter" (reicast_pvr2_filtering,
OpenGL only) is removed: PostProcessor, gles/postprocess.cpp/.h, the
PowerVR2Filter setting, the option in every language, and its
Makefile entry. It guessed at a 3-tap vertical blur, which the exact Y
filter now replaces. It also dithered by its own rules and added a made-up
"VGA signal loss" smear, which the hardware does not do. It compiled its
shaders mid-game and drew through an extra framebuffer.
RetroArch ignores old values of the option left in users' configs.

Gates (lavapipe/llvmpipe and vkd3d, not a real GPU):
- tile_test VF3tb 1x on Vulkan, at 1 and 4 samples, and on Direct3D 12,
  at 1 and 4 samples: off, filter + 565 dither (VF3tb's own
  registers), 555 dither alone, and taps 3 lines apart with fb_concat 5.
  16/16 runs are the same as tile_ref on each.
- RetroArch (Vulkan, then glcore) with VF3tb booted, frame 900, for each
  of Vulkan per-triangle, Vulkan per-pixel, OpenGL per-triangle and
  OpenGL 4 per-pixel. With the option off, the picture is the same as
  master's, byte for byte. With it on, the picture is exactly the
  option-off picture through an independent model of the filter and
  dither (Y_COEFF 0x8040, 565, dither on). No validation errors.
- Direct3D 11 and 12 rasterizers: syntax-checked with mingw, and the
  Direct3D 11 pass compiled with d3dcompiler_47 (cs_5_0). Not run.
2026-10-24 22:13:36 +02:00
LibretroAdmin 1483e718f0 rend/tile: the Y filter as one pass of its own, in place, not a halo in the walk
The compute renderer's Y filter (the flicker filter) had each of the
walk's fine tiles shade N lines above and below the lines it writes (N
the picture's height over 480), and a coarse tile's lists binned N lines
taller at each side. At 8x that is 8 lines of halo on each side of a
fine tile, and the walk did about twice its work (VF3tb at 8x on
lavapipe: walk 22.0 s with the filter, 10.3 s without); with 8 samples
at 8x the fine tile had no line left to write and the filter turned
itself off.

Now the walk and the binning are what they are with the filter off, and
the filter is a kernel of its own, PVRC_K_YFILTER, dispatched after the
walk only on the frames that have it: in place over the picture the
walk wrote. The lines a tap reaches (y - N, y, y + N) are those of one
remainder mod N, so a thread takes a column and a remainder and walks
down its lines, keeping the line above and the line itself as they were
before it wrote them, in two registers: each pixel is read once and
written once by one thread. No image, copy or readback, nothing
allocated, no kernel made in a game (it is prebuilt with the others).
The X scaler stays in the walk's resolve.

The picture is the same: tile_test (TILE_YFILTER) matches tile_ref at
1, 4 and 8 samples on Vulkan (lavapipe), and at 1 and 8 samples with
the taps 1 and 3 lines apart on Direct3D 12 (vkd3d-proton under wine),
filter off and on. tile_ref no longer turns the filter off where the
walk's fine tile had no line to write. At 8x with 1 sample the new
picture is the old one bit for bit (tile_bench's picture hash).

VF3tb 8x, 1 sample, lavapipe on one thread (a CPU's times, not a GPU's):
  filter off: walk 10.30 s
  filter on:  walk 10.63 s + Y filter pass 0.36 s (was: walk 22.0 s)

Direct3D 12 needs typed loads of R8G8B8A8_UNORM from an unordered
access view for it (every device of feature level 12_0 and up has
them); a device without them draws the picture unfiltered.
2026-10-24 16:29:32 +02:00
LibretroAdmin 3efb00129f rend/tile: the chip's X and Y scalers in the compute renderer, a core option
The PowerVR2 passes every tile through its X and Y scalers on the way
from the accumulation buffer to video memory (SCALER_CTL, Y_COEFF; the
Dreamcast System Architecture manual, 3.4.11). refsw does since the last
change; now the compute renderer does too, with the same arithmetic
(rend/tile/pvr_scaler.h), behind a new core option "PVR2 scaler /
flicker filter" (reicast_pvr2_scaler, enabled by default; the compute
renderer only - the raster renderers ignore it).

- X scaler (SCALER_CTL bit 16): the game draws twice as wide; the frame
  is drawn so, and each pair of pixels is resolved as one box of 2S x S
  samples (S the SSAA samples), with no pass or image of its own.
- Y filter (vertical scale factor above 0x400; most Sega games set 0x401
  and Y_COEFF 0x8040): each line blended with the lines above and below
  by Y_COEFF (centre bits 15:8, neighbours 7:0, in 256ths), its taps N
  lines apart at internal resolution N (the picture's height over 480),
  the picture's first and last lines in the place of the missing one.
  The walk's fine tile shades a halo of N lines above and below the
  lines it writes, the taps in its own lanes (shared memory), so there
  is no new pass, image or readback; a coarse tile's lists are binned N
  lines taller at each side to cover the halo, and its rows of fine
  tiles each write their lines less the halo (pvrc_walk_down(), so the
  variants' dispatches across, pvrc_walk_x(), take the frame's flags).
  Where the halo would be large the fine tile is made narrower and
  taller (pvrc_fine_yfilter()); Direct3D 12's table of dispatches across
  has a row for each count of lines, made at Init(). The filter is in
  spare bits of a frame word (PVRC_F_FLAGS): turning the
  option on or off mid-game compiles and allocates nothing.

Disabled, the frame words and the kernels' work are what they were, and
the picture is the same byte for byte. Nothing is added to the save
state: the registers are already the render context's.

tile_ref.c does the same, and tile_test draws each frame with the filter
off and on (TILE_YFILTER, default "off 1:8040"); tile_bench takes
BENCH_YFILTER to time it.
2026-10-24 15:47:27 +02:00
LibretroAdmin 59c1ab4b59 rend/tile: the walk's variants dispatched over the sort's lists of coarse tiles
The walk's two variants were each dispatched over every fine tile of the
picture, and each workgroup first found out whether its tile was its
variant's - the bins' counts of every chunk, and for each sorted
translucent list two binary searches in each of the coarse tile's runs -
and half of them left at once: twice the workgroups the picture needs,
each asking the same question of the same coarse tile as all the others
of it (at 8x, 5120 x 3840, two dispatches of 76800 workgroups for 76800
tiles).

The sort, which has each run of each coarse tile in hand, now answers it
once a run: whether the run has a primitive of a sorted translucent list
(each entry tested against the lists' ranges in the chunk; a run walked
over its whole chunk, whether a list has primitives in the chunk), the
same answer pvrc_tile_sorts() gave. Each run's workgroup marks its tile
and counts itself done in a word of the tile's (PVRC_B_TILES, after the
GPU's dispatch sizes in the bin buffer), the mark before the count, and
the one that counts the last run puts the coarse tile in its variant's
list - those without such a primitive from the list's start, those with
from its end - and counts it in that variant's dispatch (PVRC_ST_WALK_U_X
and PVRC_ST_WALK_S_X: y the tiles in the list, z 1). The renderers write
the dispatches' x, the fine tiles of a coarse tile (pvrc_walk_x(), from
the frame's tile size and the samples; Direct3D 12 copies it from a
small upload buffer made at Init(), with the bins' counts to the
arguments), and dispatch the variants indirectly: a workgroup takes its
coarse tile from the list and its fine tile of it from its x, and one
past the picture's edge leaves before its first barrier. Nothing is read
back, and nothing is allocated while a game runs (the bin buffer has
2 x runs words more, made at Init()).

Which tiles each variant draws is what it was, and each tile is drawn by
the same kernel's code as before, so the pictures are the same to the
bit: tile_test against tools/rend/tile_ref.c (unchanged), Vulkan and
Direct3D 12, at 1, 2, 4 and 8 samples, the frames of the decode's too,
and tile_bench's production-mode pictures against the commit before.
tile_test and tile_bench dispatch the variants the same way (tile_bench
the way each set's header has it: a file "lists" in its directory);
TILE_WHOLE=1, the whole walk alone, is dispatched over the fine tiles as
before.

On Mesa's software device (tile_bench, the kernels' CPU time, medians of
3 rounds, against the commit before): Jet Grind Radio 9 and 10 percent
less at 1x (1 and 4 samples) and 10 at 8x, Virtua Fighter 3tb 15 and 16
at 1x and 16 at 8x, Virtua Fighter 4 15 and 16, Sega Rally 2 5 and 9 at
1x and 12 at 8x, Dead or Alive 2 16 and 20 at 1x and 22 at 8x, the
render-to-texture disc's texture 11 and 15. RADV's code for a Navi 31:
the walk's variants keep their registers (84 and 96 at one and four
samples for the tiles without sorted translucent fragments, 120 and 192
for the others).

The SPIR-V and the DXIL made again.
2026-10-24 14:38:53 +02:00