62 Commits
Author SHA1 Message Date
libretroadmin 195f56511a Renders to textures: the screen's attachments, and only their own pixels
A render to a texture draws with the screen's attachments - the depth
with the per-triangle and per-strip renderers, the colour and depth the
per-pixel renderer keeps between passes - made at least as large as a
render of a screen, 640x480 times Render To Texture Upscaling
(TextureDrawer::ShareExtent()). It draws its own pixels, not the whole
power-of-two texture they are in; the rest is cleared black once, when
the image is new or was written since (Texture::ClearOutside()).

Nothing is made for a render during play, and the video memory is the
screen's: no more than before renders to textures had attachments made
up front, and up to 16 MB less than with them, at any resolution where
the screen is at least as large as the renders. A render larger than the
screen has attachments of its own made when it comes, logged. With SSAA
or the upscaler the texture drawers keep their own.

tools/vulkan/vk_vram.sh: the test disc built with -DLIVE_ODD, a render
of no power of two drawn as its whole texture, each raster renderer at
1x, 2x and with 2x renders: every render drew with the screen's
attachments, black past it (tools/rend/odd_check.py), validation clean,
the image memory at the most printed (vk_hdr VKH_IMAGE_BYTES). In CI.
2026-11-10 06:25:56 +01:00
libretroadmin 53bc61f4d8 Renders to textures: attachments and read-back room made before play
The texture drawers' attachments are made with them, as large as a
render to a texture can be (TextureDrawer::MaxSide()), as SSAA's already
were: the per-triangle drawer's depth, and the per-pixel drawer's colour
and depth with their framebuffers (OITDrawer::MakeAttachments(), split
from MakeBuffers()), each with a framebuffer's place for every image. The
room a render read back into video memory is put in is reserved before a
renderer's first frame for one of the screen's size (rtt_watch_reserve()).
A game's first render to a texture no longer makes them mid-game.

Four sites come off the list of allocations during play, 22 left.
2026-11-10 03:57:13 +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 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 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 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 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 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 0c4f0dcc46 Vulkan: set the modifier volumes' and prepass's cull mode with the draws
Where the device has VK_EXT_extended_dynamic_state, the modifier volume
and depth prepass pipelines are made once for the three cull modes, with
the cull mode dynamic, and a block of their draws sets it when it
changes. Without it they are made for each cull mode as before; the
choice is made once when the renderer starts.
2026-10-28 11:06:39 +01:00
LibretroAdmin 5ddb6041a8 Sort translucent triangles straight into the index buffer
GenSortedTo() and GenPrepassTo() write the indices to a pointer, with
room for them from SortedTriangles(), once each and in order. Vulkan,
Direct3D 11 and Direct3D 12 sort into the mapped buffer the card reads
them from - from the vertices as the game sent them, before their
colours are moved - instead of into vectors copied after. The vertices,
indices and volumes keep their copy: the CPU reads them again.
2026-10-28 09:59:40 +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 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 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 70304fcd02 vulkan: the chip's write-out before the VMUs and crosshairs, as Direct3D and OpenGL have it
On Vulkan the VMU screens and lightgun crosshairs are drawn in the
frame's render pass (its last subpass), so the write-out pass, recorded
after the render pass, filtered and dithered them too. On the console
they are not part of the picture at all; Direct3D and OpenGL already
draw them after the pass.

Now, when the frame has a write-out and there is something to draw over
it, the screen drawer ends the frame's render pass, records the pass,
and the VMUs and crosshairs go in a render pass of their own that
loads the picture (VulkanWriteOut::BeginOverlay()), with a quad pipeline
made for it. The render pass, its framebuffers (one per picture) and
the pipeline are made with the pictures, when the renderer starts or
changes size. Nothing is copied, and nothing is made in a frame. A frame
without a write-out, or with nothing over the picture, records what it
did before. With the option off, nothing changes.

Gates (lavapipe; not a real GPU): RetroArch, VF3tb booted to frame 900
with VMU screen 1 shown (opaque), for the per-triangle and per-pixel
renderers:
- option off: the same picture as before, byte for byte;
- option on: outside the VMU screen (and the line under it, whose
  filter taps reach it), the option-off picture through the model of the
  filter and dither; on the VMU screen, the VMU as drawn, unfiltered.
  No validation errors.
2026-10-24 22:13:48 +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 ae601c3aaa rend: a modifier volume triangle with a vertex far off the screen is cut before the GPU clips it
On the user's Jet Grind Radio frame (the jgr13 state) the per-pixel and
per-triangle renderers drew the street's shadow with a dark band down the
right of the picture, 51 pixels wide at every resolution (x from 589 of
640 at 1x, from 4709 of 5120 at 8x), and Direct3D 12 a strip along the
top as well - in Vulkan and in Direct3D 12 (vkd3d-proton), on lavapipe,
on master. refsw and the compute renderer, which take the exact
geometry, draw neither (tools/rend/tile_geo.c holds the compute
renderer to it; refsw is held to it since its far-out triangles are set
up exactly).

The shadow volumes have vertices 1e9 pixels out, at 1/w 1e5 (w 1e-5),
and the raster renderers give them to the GPU as they are: a clip-space
vertex of (30, 30, ., 1e-5). The GPU clips such a triangle to the screen
in 32-bit float, and where an edge between two of those vertices crosses
the screen's side, the point it finds comes from coordinates of 30 that
cancel to 1e-5: its place on the screen is out by a fifth of the
screen's width. The cut polygon's side that lies on the screen's right
edge came out at x 589, so the volume triangle missed the strip to its
right, the strip was outside one face too few, and it was shadowed.
(A float32 model of the clip of the frame's volume triangles puts their
sides that far off; the band's edge is straight down at x 589 in every
row, the clip plane's side, not the line of any edge of the volumes.)

Now, while a raster renderer is drawing (ta_set_modvol_clip(), set in
each one's Process), a frame whose volume triangles have a vertex more
than 32768 pixels from the screen's origin has all its volume triangles
made again for the raster renderers (rend/modvol_clip.h, into
rend_context's mv_clip and mv_clip_range): each far one cut, in double
precision, to the square of that size about the origin and given as the
fan of up to five triangles the cut polygon makes; 1/w, which is linear
across the screen, taken along each edge. Every point of the fan is on
the triangle's edge to well under 1/256 pixel, the fan's triangles keep
its winding and share their edges exactly, and an edge two triangles
share is cut to the same points in both (from its ends in one order), so
a sample is inside one face of a volume or another as before, never in
two and never in neither. The GPU then has vertices no farther out than
the square, which its float clipping keeps exact. A volume left with no
triangle keeps one with no area, so its closing draw is still made.

It is made on both of the TA's ways: from the parse's triangles, and on
GPU-side TA processing from the frame's blocks (as the shadows' bounds
are, ta_gpu_shadow_areas()), so the raster renderers then draw the
volumes from the CPU's triangles instead of the decode's. A frame with
no volume triangle that far out - most games, most frames - is drawn as
before, to the bit: nothing is made, uploaded or drawn differently. The
compute renderer, refsw and the tools take modtrig as before.

The renderers draw each volume from its range in mv_clip when the frame
has it (mv_clip_range_of()): Vulkan (drawer.cpp, oit_drawer.cpp), OpenGL
(gles/gldraw.cpp, gl4/gldraw.cpp, gl4/abuffer.cpp), Direct3D 9, 11 and 12
(their renderers and per-pixel renderers). No shader changes; no new
allocation when running: the lists are made with the TA context, room
for half as many triangles again as modtrig, and a frame that would need
more is drawn as before.

tools/pvr/ta_gpu_test: the cut triangles made from the blocks are held to
the parse's, byte for byte.

On jgr13 with the user's options (SSAA 2x, which lavapipe draws as 1,
TA processing on the GPU, threaded rendering): the band is gone from
Vulkan's per-pixel and per-triangle pictures at 1x and 8x, from Direct3D
12's (per-pixel and per-triangle at 1x, per-pixel at 8x), and from
OpenGL 4's per-pixel and the per-triangle OpenGL one at 1x; Direct3D 12's
per-pixel picture is now within 8 levels of Vulkan's at every pixel, and
Vulkan's within 2942 pixels (more than 32 apart: texture filtering) of
the compute renderer's, from 25405. TA processing on the CPU draws the
same picture as on the GPU. Virtua Fighter 3tb and Jet Grind Radio's
select screen (no volume that far out) draw the same pictures to the bit.
Direct3D 11 and 9 are built and their volume draws changed the same way,
but not drawn here.

Gates: tools/rend/speckle_test.sh with jgr13 and the select screen state
(sources and device runs) PASS; tools/pvr/ta_gpu_test.sh (Vulkan, OpenGL,
jgr13 and s10: the cut triangles from the blocks the parse's, 2 frames
with some) and its Direct3D 11 and 12 runs PASS; tools/rend/tile_test.sh
with tile_geo PASS; texdecode_test PASS; tools/determinism deterministic;
tools/threads/headless.sh passed; kernels_spv, kernels_dx12 and
kernels_dxbc check (no shader changed).

Verified on lavapipe (Vulkan), llvmpipe (OpenGL) and vkd3d-proton on
lavapipe (Direct3D 12) only; unverified on a GPU, where the clipper may
differ (the band may be narrower, wider or not there at all on the user's
RTX 5090), and the cut geometry is the exact one whatever it does.
2026-10-24 11:36:58 +02:00
LibretroAdmin 4fcbf561c8 vulkan: the depth is the position's, as the chip has it, in every pass
The raster renderers wrote a depth of their own from the fragment
shader, log2(1 + 100000/w)/34, in every pass, and tested EQUAL between
passes: the per-pixel renderer's colour pass over its depth pass, a
polygon drawn again for its shadow, the depth prepass of a sorted
translucent list under its colours. Each pass has its own fragment
shader and so its own pipeline, and a driver may round the same
expression differently in each one: where a pass's depth missed the
other's by a unit in the last place, the EQUAL test failed and the clear
colour showed through decals and shadow blobs as black specks. Only on
real GPUs - a software device compiles every pipeline alike - and only in
the raster renderers: the compute renderer derives a depth once. The
reference renderer (tools/rend/refsw) says how the chip has it: 1/w as a
32-bit float, interpolated by a plane equation in screen space, compared
once in the ISP; the layers of the scenes with the specks are thousands
of units apart, so they were never confused.

Now the depth is the vertex position's, by the fixed functions, and no
fragment shader writes one. A vertex goes out as (x*w, y*w, S, w) with
w = 1/z, so the depth the device interpolates is S*z: the chip's 1/w,
linear in screen space, in the D32 float depth buffer with larger nearer
(reversed Z, the viewport's depth range 0 to 1), the eight depth modes
the device's own compare operations as before. S is 1/fZ_max, the
parse's; a frame decoded on the GPU has fZ_max only on the GPU, so the
decode writes 1/fZ_max into a word of its frame (TAG_O_ZSCALE), and the
drawers copy that word over the host's into the vertex uniforms before
the render pass - nothing read back. The vertex uniforms are a small
buffer of their own for that, one an image, written before the render
pass begins (the main buffer's layout is only known after). The vertex
shaders clamp the position's z to w, so a z beyond fZ_max is at depth 1
and not clipped; the matrix's z scale (ExtraDepthScale) is undone for
the depth by a second uniform.

One text, shaders/position.glsl, places the vertices of every vertex
module (polygons, modifier volumes, the per-pixel renderer's), from the
same uniform block, and declares gl_Position invariant: the depth pass,
the colour pass, the shadow redraw, the prepass and the modifier volumes
are placed identically by construction, so EQUAL holds. Where a fragment
shader keeps a depth in the per-pixel lists or compares one (the
translucent modifier volumes, the lists' manual depth test) it is
gl_FragCoord.z, the same value the depth test took; fog keeps
gl_FragCoord.w. The modifier volume fragment shaders are empty but for
the final pass's tint. With no depth written by a fragment shader, early
depth testing works in every pass; on lavapipe the per-triangle
renderer's frames are 10-25% less GPU time, the per-pixel renderer's
10-35% (Jet Grind Radio, three scenes).

Ties stay as they were: draw order (opaque: later wins on >=; sorted
translucency: the stable sort), as refsw does.

raster_spv.inc made again (tools/rend/kernels_spv.sh), the GPU-side TA
kernels too for the new word; tools/pvr/ta_gpu_test compares the word
with the parse's 1/fZ_max.

tools/rend/depth_test.sh shows the mechanism on a device (lavapipe): a
floor drawn and drawn again by another pipeline with EQUAL misses no
pixel with the position's depth, nor with the old fragment depth while
both passes round alike, misses them all when the second pass's
fragment depth is a unit off (what the driver did), and misses many
when the second pass's position is a unit off (why the position is one
invariant text); and it reads the shipped modules: every polygon vertex
module declares its position invariant, none writes gl_FragDepth.

On lavapipe the pictures of Jet Grind Radio's select screen, train and
street scenes (per-pixel and per-triangle, TA processing on the CPU and
on the GPU, the validation layer on) are the same pixels as before: the
new depth orders every pixel as the old one did there, and so is as
close to refsw as before (per-pixel: mean 4.29, 3.11, 5.37; the compute
renderer 3.55, 2.92, 5.19). What changes on real GPUs - the specks - is
to be seen there.
2026-10-20 01:52:39 +02:00
LibretroAdmin 4ad3ad4316 pvr: the depth prepass leaves out on the GPU the strips no vertex makes opaque
GenPrepass() leaves out a polygon whose alpha is its vertices' (or
theirs times the texture's) when none of its vertices has 255
(PrepassVerticesOpaque()): no pixel of it can hide what is behind it.
The header pass of GPU-side TA processing has not got the vertices, so
its prepass drew such polygons and the shader discarded every pixel.

Now the CPU writes, after the prepass's indices in the sorted indices'
buffer, a check for each translucent strip in the prepass whose alpha
is its vertices' (PrepassAlphaFromVertices(), the test
PrepassVerticesOpaque() starts with): its vertices and where its
triangles are among the prepass's (TAG_T_PREP, TAG_PC_*). A new kernel
(TAG_K_PREPASS), dispatched after the unpacking and before anything
moves the colours, looks at each strip's vertices, a workgroup a strip,
and where none that is drawn has an alpha of 255 makes its triangles
(a, b, c) into (a, a, a), which draw nothing. The draws and their
counts stay the CPU's; nothing is read back. A translucent strip is a
polygon parameter of its own, so this is GenPrepass()'s choice exactly.

The sorted indices are written when the decode is recorded, before the
kernel that reads them (they were written when the frame was drawn).
Vulkan and OpenGL both: the buffer is bound to the kernel as set 1
binding 10 and binding 13.

tools/pvr/ta_gpu_test: GenPrepass() is given the vertices' own alphas
again; the checks are run on the device and by the model, which must
agree word for word, and the prepass's draws must be GenPrepass()'s as
before, a draw all of whose triangles were left out being one it has
not got. It counts the area of what is left out.

The prepass's area per sorted frame at 640 x 480 (ta_gpu_test, the
device's vertices): Jet Grind Radio s9 13 of 1390264 pixels left out,
s10 107714 of 583534 (18%), Virtua Fighter 4 29384 of 268756 (11%), Sega
Rally 2 15156 of 285594 (5%), Dead or Alive 2 none.
2026-10-16 02:43:46 +02:00
LibretroAdmin 7791baf886 rend: the texture pool's rectangles and renders; Vulkan's upload ring made once
The texture pool (rend/tex_pool.h) made square shapes only at the start;
a rectangle had its image made the first time a game used one. A quarter
of the pool's share of the budget now goes to rectangles of every shape
from 8 to 1024 texels a side, by fixed shares: the smaller and the nearer
to square, the more (64x32, 128x64, 256x128 a few each; strips and the
large ones made when they are first used, as before, and counted).

Pictures rendered to are a kind of format of their own (TARGETS): a
fixed few of the shapes renders come in - 64x64 to 512x512 two of each,
1024x1024, four 1024x512 (a 640x480 render), two 512x256 and one of each
other two-to-one shape from 64 - half as many on a card with little
memory. The Vulkan renderers render to one of those
(Texture::CreateRenderTarget(), from both drawers), RGBA8 with the usage
a render needs; the Direct3D 11 ones take the spare a render is drawn
into from them (DX11Texture::TakeSpare()), with its render target view
made at the start, and give it back when the texture is done with it.
A render at a resolution above 1x is larger than any of them and is made
as before.

The Vulkan renderers' upload ring, where textures converted on the CPU go
to the GPU through, made 4 MB buffers when a frame needed more, and gave
them back after some ten seconds unused. It is one buffer now, made when
the renderer starts, a share of the budget (16 to 64 MB), handed out a
piece at a time, the frames' pieces one after the other. A frame's pieces
are free again when its place comes round (the texture command pool has
waited for its fence then); when the ring is full, the oldest frame
still out is polled (CommandPool::IsDone()) and its pieces taken back if
it is done. Nothing waits: with no room, the texture is made with the
next frame instead - marked dirty, its image (made one that can be
sampled, if it was new) left as it is until then.
2026-10-16 01:59:35 +02:00
LibretroAdmin 0102585127 vulkan: pipelines made at start and on a thread, never in a frame
Both raster renderers made a polygon's pipeline the first time a polygon
of its kind was drawn, inside retro_run: 15 (per-triangle) and 19
(per-pixel) of them in Jet Grind Radio's first 24 frames, 30 with SSAA,
and one more whenever a game drew something new later on. The modifier
volumes', the per-pixel renderer's last subpass and clear, and the
quads' pipelines were made the same way, when first drawn. Now no
pipeline is made in a frame (core/rend/vulkan/pso.h).

Measured with uprobes on lavapipe (2 CPUs), Jet Grind Radio from a save
state, 300 frames. "Cold" has no pipeline cache and an empty Mesa shader
cache; "warm" is the next run, with both kept.
                         pipelines made   first 24 frames    largest of them   context reset
                         in retro_run     cold     warm      cold     warm     cold    warm
  per-triangle, GPU TA,  35 -> 0         14350 ->  3412 ->   8331 ->  515 ->   3669 -> 3182 ->
  threaded                                9589 ms  1936 ms   4911 ms  315 ms   2561 ms 1223 ms
  per-triangle, CPU TA   35 -> 0          8614 ->  3659 ->   3679 ->  846 ->    969 ->  841 ->
                                          4007     2297      1600     428       277     427
  per-pixel, GPU TA      47 -> 0         21981 ->  6432 ->  12046 ->  866 ->   2682 -> 4429 ->
                                          9411     2768      4380     296      1602    2315
  per-triangle, SSAA 4x  59 -> 0         24992 ->  8182 ->  10083 -> 1445 ->   4828 -> 3467 ->
                                         13004     4363      4464     603      1913    1190
("Pipelines made in retro_run" counts the GLSL compiles and the
pipelines, the modifier volumes' and the quads' included.) The main
thread's own CPU time over the 300 frames is about the same (1289 ->
1144 ms in the first case). The largest cold frame
left is lavapipe's: it compiles a pipeline's code for its rasterizer when
the pipeline is first drawn with, on its own thread, and on two CPUs that
competes with the pipeline worker below.

Keys. A polygon's pipeline is kept under a 64-bit key of everything it
is made of, worked out from the polygon (PipelineManager::Key(),
OITPipelineManager::Key()), and every pipeline is built from its key
alone:
- per-triangle: the shader's 15 bits (vulkan/pso_key.h), Gouraud, SSAA's
  sample rate (none, at each sample, once a pixel), cull mode, list or
  strips, depth write, depth compare op, the stencil state (none, shadow
  bit set, shadow bit cleared, the two shadow redraws), blend enable, and
  source and destination instructions;
- per-pixel: the shader's 14 bits, Gouraud, sample rate, pass and
  subpass, cull mode, depth write, depth op, stencil state (none, set,
  cleared), blend enable, colour writes, source and destination.
The states are stored as they are made, not as the polygon's register
bits, so polygons that differ only in bits the pipeline ignores now share
one. Three key collisions in the old hashes are gone with this:
- the per-triangle key had no trilinear bit, so a trilinear polygon could
  take a non-trilinear polygon's shader, or the other way round;
- the per-pixel key had no blend factors for a two-volume polygon, and
  could not tell apart unsorted translucent polygons that write depth in
  the colour pass from sorted ones;
- the sorted-triangle topology came from a global setting read when the
  pipeline was made, and a change of the setting dropped every pipeline in
  the middle of a frame. It is now part of the key, and nothing is dropped.

The general pipeline. The first time a key is drawn, its pipeline is
asked of a worker thread, and the polygon is drawn at once with its
renderer's general pipeline. That is the same SPIR-V with the
specialization constant left at -1, so the shader reads the polygon's
bits from the push constants (the per-triangle block gets a seventh word
at offset 24, the per-pixel one a word at offset 96). Cull mode,
topology, depth test, depth write and compare op, and the stencil test,
ops, masks and reference are set with the draw (VK_EXT_extended_dynamic_state),
and so are blend enable, equation and colour write mask
(VK_EXT_extended_dynamic_state3). The core enables both extensions, and
VK_EXT_pipeline_creation_cache_control, on the device it makes when the
GPU has them. There is one general pipeline for each module and subpass:
2 for each per-triangle pipeline manager (the screen's and the
textures'), 8 for each per-pixel one (4 and 14 with SSAA). They draw the same pixels as each polygon's own pipeline: every
screenshot of the test runs drawn with only the general pipelines (a
test build) is the same, byte for byte, as with the polygons' own, with
both dynamic states, with the depth ones only, and with neither.

A device without the dynamic states has a general pipeline for each fixed
state it is drawn with. Those of the keys kept from earlier runs are made
at start. One not made yet is made there and then, the draw waiting for
it. That is the one case where a draw waits, once for each such state;
the log counts them. Without dynamic depth and stencil this covers cull,
depth and stencil states; without dynamic blend, the blend factors.

When. At renderer start (context reset), the general pipelines, the
modifier volumes' (all modes and cull modes), the depth prepass's (12,
24 with SSAA), the per-pixel renderer's translucent modifier volumes',
last subpass and clear, and the quads' are made by the drawing thread and the worker together, sharing out one
list: everything a frame can need. The polygons' own pipelines are made
on the worker, the draws' first, then all the keys kept from earlier
runs (prefetched). A new size of screen keeps every pipeline: the
per-pixel renderer used to drop its polygons' pipelines on every Init().

The worker runs below normal priority (nice 10 on Linux and Android,
below normal on Windows): what it makes is never waited for, and where
there are fewer processors than busy threads the frame goes first.

Lock-free. A slot's state is an atomic the worker publishes the pipeline
with (release; the draw reads it acquire). Requests go on two intrusive
lists (cMpscList: draws' and prefetches') and the worker sleeps on an
eventcount. A manager that starts again (render pass, samples) bumps a
generation, waits for the worker to answer what it was asked, and only
then destroys its pipelines. The worker drops stale requests without
building them. The tables are made with their manager; nothing is
allocated in a frame. Without threads (NO_THREADS=1) there is no worker,
and polygons are drawn with the general pipelines.

The cache. The worker has its own VkPipelineCache, made from the same
data as the context's, and both are created EXTERNALLY_SYNCHRONIZED where
the device allows, so the driver takes no lock for them. The file
(system/dc/vulkan_pipeline.cache) now starts with a header and the keys
of the pipelines made (PsoKeyLog), then the device's data. A file
without the header is read as before, as the device's data alone. It is
saved, with the worker's cache merged in, when the game is unloaded and
again when the context goes, if pipelines were made since. It is
written beside the old file and then renamed over it.

Draws. An untextured polygon drawn with a general pipeline needs a valid
texture binding, so each frame's descriptor sets bind one (the fog
table's view) as set 1 until a texture's replaces it. The per-pixel
renderer always writes the second texture binding (the first texture
when the polygon has one volume). The per-triangle renderer's trilinear
factor is pushed for every trilinear polygon; it used to be skipped when
it was 1.0, and the shader then multiplied by whatever was pushed last.
2026-10-15 20:24:11 +02:00
LibretroAdmin 1fa4c874db vulkan: the textures' images from a pool made when the renderer starts
A texture made its image when it was first used, and again when its
size or format changed: in the middle of the game, a scene's worth of
them in the frame it loads, and that was most of the time spent updating
textures.

rend/tex_pool.h keeps images by class - format, width, height (powers of
two from 8 to 1024) and whether it has its mipmap levels - with a free
list for each, threaded through the images' own entries: nothing is
allocated to take or give one back. How many of each class are made at
the start is a share of the video memory budget (a sixteenth of what is
left, 16 to 64 MB), spread the way games' textures are: mostly 16-bit,
square and mipmapped, 64 to 256 texels. A class with none left has one
made then, which stays in the pool: counted, and said once for that
class, as a sign the share is too small for the game. Only the thread
that renders touches it.

The Vulkan pool's images are optimal, sampled, copied to and from, and
storage images with a view of each level where the decoder takes their
format, so one image serves the CPU's upload, the kernel and the copy of
a planar texture alike. Their memory comes from the heap's blocks, which
are taken when the pool is made. A cache texture takes one in Init()
when the shape is one of the pool's; an image given up while frames that
may use it are in flight (DestroyLater(), a texture that becomes a
render target) goes back when the frame's place comes round again, one
whose texture has been deleted goes back at once. The renderer's own
pictures (fog table, palette, framebuffer, VMU screens) are not pooled.
The pool's summary is logged when the renderer goes.
2026-10-15 20:23:50 +02:00
LibretroAdmin b5a3caa3ae vulkan: the depth prepass of sorted translucent polygons, GPU decode too
The prepass of rend/sorter.h for the Vulkan per-triangle renderer, by
triangle and by strip, and for frames whose vertices the GPU decodes and
whose triangles the CPU sorts (TA_GPU_SORT).

Its pipelines are all made with the render pass, never in the middle of
a frame: one fragment shader that works out a polygon's alpha as the
colour pass's does - the same expressions in the same precisions, with
what that one has as macros read from the push constants (where the
colour pass has its trilinear alpha, which no polygon of the prepass
needs) - made textured or not, flat or Gouraud, and at each SSAA rate;
pipelines for each of those and each culling, depth test and write on,
no colour writes. The push constants the colour pass left are put back
after it.

Pipelines made (PipelineManager::CreatePrepassPipelines(), for the
screen's and the render-to-texture pass): prepassPipelines[textured]
[Gouraud][no culling, front, back][per-sample rate], 12 without SSAA and
24 with, from 4 (8) prepass shaders (ShaderManager::GetPrepassShader()).
They are not made again when the way of sorting changes.

GPU decode: ta_gpu_prepass(), the sorted passes' triangles grouped as
GenPrepass() does, from their headers (no vertex alpha looked at: the
vertices are the GPU's), their indices after the sorted ones in the
decoder's room, which is twice what it was.
2026-10-15 18:53:29 +02:00
LibretroAdmin 24cb5e2e51 vulkan: SSAA in both raster renderers
A new core option, SSAA (reicast_ssaa): Off, 2x, 4x, 8x; Off by default.
Each pixel of the Internal Resolution is drawn from that many samples
and resolved by the shared rule (rend/tile/pvr_resolve.h). It is taken
up when the renderer starts and when the option changes, which is a
change of size (Resize()); nothing is made, compiled or read back while
a game runs for it.

How many samples (VulkanContext::SsaaSamples()): the most of those asked
for that the device can draw colour, depth and stencil with
(ssaa_device_samples()), and then that the video memory budget the
context read at start-up has room for (ssaa_clamp_samples()): the
screen's colour and depth, per sample, after the per-pixel renderer's
lists; nothing per sample for the per-triangle renderer where the device
has lazily allocated memory, since its sample attachments are transient. One line in the log says what was asked and
what was taken. It needs sample rate shading, which the core enables on
the device it makes (VkCreateDevice); a device the frontend made without
the core gets no SSAA, and says so.

The per-triangle renderer (drawer.cpp, pipeline.cpp):
- The screen's and the textures' render passes (MakeSsaaRenderPass())
  draw into colour and depth attachments of N samples, transient, cleared
  and never stored (load CLEAR, store DONT_CARE), and a second subpass
  resolves the colour into the picture (SsaaResolve, quad.cpp): a
  triangle over the framebuffer reads each sample through a
  subpassInputMS, as the unorm8 the attachment holds (packUnorm4x8()),
  and writes (sum + N/2) / N per channel. Never the device's resolve.
  The picture is all written by it, so it is not loaded.
- Pipelines get the sample count and a rate bit in their key (bit 30):
  shaded at each sample (sample shading, minSampleShading 1, the inputs
  "sample") or once a pixel. Once a pixel are the polygons drawn at a
  single rate (SsaaSingleRate(): sprites, the background, and flat strips
  where the decoder looks for them); their inputs are taken at the
  pixel's centre (interpolateAtOffset(v, 0)) where they cover all of a
  pixel's samples, which is where a frame without SSAA takes them (with
  more than one sample the device may take them elsewhere), and at the
  centroid where they cover only some, so that an edge pixel is not
  shaded from beyond the polygon. Both rates of a key are made together,
  so SSAA adds no compile of its own to the lazy creation that is there
  already.
- The last picture under a partial render, and modifier volumes, go into
  the samples; the VMUs and the OSD are drawn after the resolve.
- Renders to a texture are supersampled only where their sample
  attachments cost nothing (lazily allocated memory: tilers), with the
  attachments made at start-up as large as such a texture can be (1024 x
  the render-to-texture upscale a side). Elsewhere they are drawn with one
  sample, as without SSAA: at 4x that preallocation is 48 MB at 1024 x
  1024, more than the whole of what brute force (twice the Internal
  Resolution) adds, and what a game reads back of such a render stays what
  it is without SSAA.

The per-pixel renderer (oit/):
- Four subpasses with SSAA (RenderPasses::MakeSsaaRenderPass()): 0 depth
  and stencil, 1 colour and the translucent lists, 2 the translucent
  modifier volumes, 3 the picture put together and resolved, in one
  pass: the lists are read and sorted once a pixel, blended at each
  sample onto that sample's opaque colour, and the samples resolved by
  the shared rule. The sample attachments are one image each for all
  the render passes of a frame: a frame of one render pass never stores
  them; one that is not the last puts each sample together into them in
  subpass 2 (a per-sample variant of the same shader) and writes the
  translucent depth there, for the next render pass to draw over. They
  are not transient, since such a frame stores them.
- Textures are rendered to with one sample a pixel, as without SSAA,
  with shaders of their own (rttShaderManager): see the per-triangle
  renderer for why.
- A translucent fragment of a sorted pass is put in the lists once a
  pixel, with the samples it covers and is in front of the opaque depth
  at in eight bits of its polygon number's word (polygon numbers are
  under 2^22 then): the lists do not grow by N. Where triangles that
  share an edge cross a pixel each has a fragment there, with the
  samples that are its own; small triangles make a pixel's lists longer
  that way, and the most fragments sorted a pixel goes from 32 to 96
  with SSAA (without it, unchanged). Translucent modifier volumes count
  where they cover a pixel's first sample, so that two triangles of a
  volume sharing an edge count once.
- Stencil and depth are read as subpassInputMS at the sample a shader
  runs for (or the first one it covers, once a pixel).

Shaders without SSAA are built from the same text as before but for the
names of their inputs: a frame with SSAA off is drawn as it was (checked
against the build before, byte for byte, below).

GPU-side TA processing: the GPU's decode does not look for flat strips
(it would have to read every vertex on the CPU to choose a pipeline), so
with TA Processing = GPU only sprites and the background are drawn once
a pixel, and flat strips are shaded at each sample; ta_set_flat_strips()
is asked for only when frames are parsed on the CPU. The two give the
same picture with SSAA off, and pictures that differ by that with it on.

The log, for measuring: the GPU time of the screen's frames (two
timestamps a frame, read when the frame's slot of the command pool comes
round again, after its fence; never waited for) as mean, 99th
percentile and longest at the end (GpuFrameTimer), and the video memory
in use then (LogVramUsage()).

No change to the save state; run-ahead stays exact.

Tested on lavapipe (Mesa's software Vulkan, samples 1 and 4 only: 8x is
4x there, 2x off), RetroArch with the validation layer, over saved and
booted scenes (Jet Grind Radio, Sega Rally 2, Virtua Fighter 4, the live
test's disc), both renderers, threaded rendering on and off, TA
processing on the CPU and the GPU, run-ahead, Internal Resolution 2x:
- no validation error in any of them, SSAA on or off;
- SSAA off: the screenshots are the build before's, byte for byte;
- the live test's disc (2D: sprites and flat polygons, two render passes,
  two volumes, modifier volumes, a render to a texture, punch-through and
  sorted translucent polygons) with SSAA 4x: the build before's picture,
  byte for byte, in every one of the eight ways it was run;
- tools/threads/headless.sh, with and without reicast_ssaa=4x: the
  verdicts and the sound of the build before; ta_gpu_test and
  texdecode_test pass.

GPU time (the log above; lavapipe, so the figures only show the
direction), Jet Grind Radio, 175 frames, against brute force - SSAA off
at twice the Internal Resolution, the same samples a pixel:
- per-triangle: SSAA off 38.0 ms mean (p99 111.1), SSAA 4x 128.1
  (p99 174.9), brute force 111.0 (p99 154.6);
- per-pixel: off 110.9 (p99 155.7), 4x 472.1 (p99 575.0), brute force
  369.1 (p99 441.5).
A software rasteriser shades every sample of a pixel as it shades a
pixel, has no compressed samples and no faster MSAA path, so SSAA cannot
win there; the comparison that decides it is on the NVIDIA machines.
Video memory, worked out from the attachments made (lavapipe's heap is
the host's, shared): SSAA 4x at 640 x 480 adds 12 MB to the per-triangle
renderer (colour and depth of 4 samples, 14.7 MB, in the place of one
sample's depth) and 10 MB to the per-pixel one (in the place of its two
colour attachments and its depth); brute force adds 15 to 22 MB and 26
to 30 MB (every image at four times the pixels - the two or three
pictures of the frontend's ring, depth, and for the per-pixel renderer
its colour attachments and list heads), and the frontend's own scaling
on top.
2026-10-15 15:32:49 +02:00
LibretroAdmin 65295bfb80 vulkan: the per-triangle renderer's sorted passes and shadows from the GPU
With TA Processing = GPU, the per-triangle renderer now asks the header
pass for TA_GPU_SORT (unless translucent polygons are sorted by strip)
and TA_GPU_SHADOWS:

- Sorted passes: the CPU's sort writes its indices straight into the TA
  decoder's room for them (one slice a texture-pool slot, host-visible,
  made at init; a slot is written again only after BeginFrame() has
  waited for its fence, which the draws that read it were submitted
  with). DrawSorted() binds that slice as the index buffer and draws the
  runs as it draws GenSorted()'s, with the GPU's vertices, and binds the
  decode's indices back. No vertex or index of the frame is uploaded
  except the sorted indices themselves (12 bytes a triangle).
- Shadows: DrawModVols() takes the volumes' bounds from the header pass
  and scissors the second drawing to them as for a CPU frame, and
  DrawShadowed() draws each group by the second command TAG_K_SHADOW
  wrote, which has no indices where the group does not reach into the
  volumes.
- A frame decoded on the GPU with a sorted pass always draws it from the
  CPU's sort (it never falls to SortPParams(), which needs the CPU's
  vertices).

The decoder's command buffer doubles for the second commands, and the
decode records TAG_K_SHADOW after the draw commands. The per-pixel
renderer does not ask for either and is unchanged.
2026-10-15 13:49:19 +02:00
LibretroAdmin f153f936ec vulkan: both raster renderers draw from the GPU's TA decode
With TA Processing = GPU, a frame the GPU decodes is no longer also
parsed on the CPU: the header pass makes the render context ready to
draw in the full parse's place, and the per-triangle and per-pixel
renderers draw it from the decode's vertex, index and modifier volume
buffers with indirect draws whose counts the GPU wrote. Nothing is read
back, nothing is uploaded for it from the CPU, and nothing is allocated
or compiled while a game runs.

The header pass (ta_gpu_prepare(), TA_GPU_DRAW):
- The polygon parameter lists hold a draw group each, in the lists'
  order. For opaque and punch-through, a group is what make_index()
  joins, and its first is the slot of its draw command. For
  translucent, a group is a run of headers in a render pass that are
  drawn the same way; its strips stay separate strips, and its first is
  its place in the translucent draw records: a count word, then room
  for a draw a vertex. The render passes count the groups. The modifier
  volumes and the clipping rectangle are the header pass's; fZ_max is
  the GPU's, and the Vulkan renderers do not use it.
- A frame the full parse would drop for want of room is left to it,
  since only the CPU can keep the last picture on the screen. That
  covers vertices, modifier volume triangles and volumes exactly, and
  indices by at most four a vertex. Polygon parameters are bounded by a
  strip a vertex, or, where that is too many, by counting the strips'
  end bits, one word a vertex.
- Also left to it: frames the parse skips (frame skipping), the flat
  strips and the region table, and a parameter that comes where a strip
  has not ended (the parse takes it for a vertex, the decode for what it
  is). With TA_GPU_NO_SORT, so are frames that may have a sorted pass.

The decode (rend/ta_gpu.h) gets two kernels:
- trdraw: each translucent strip's record copied to its group's place,
  with the group's count, and its polygon parameter's words for the
  per-pixel renderer. With TA_GPU_INSTANCE, firstInstance is the
  strip's place in the list.
- provoke: the colours of a flat-shaded triangle's last vertex moved to
  its first, as SetProvokingVertices() does on the CPU, in its order of
  reads and writes.

The drawers (BaseDrawer::BindGeometry(), DrawIndices()):
- Opaque and punch-through groups: one vkCmdDrawIndexedIndirect() of
  the group's command.
- Translucent groups: one vkCmdDrawIndexedIndirectCountKHR() of the
  group's records. Without the extension, one multi-draw of the group's
  whole room, whose records past the count are zeroes. A device with
  neither keeps TA processing on the CPU. There are never more calls
  than the CPU path makes draws, and consecutive strips drawn the same
  way are one call.
- The per-pixel renderer reads the translucent polygon parameters from
  the GPU's buffer and numbers each strip by its draw's firstInstance.
  Without drawIndirectFirstInstance it takes TA_GPU_NO_SORT.
- The per-triangle renderer sorts translucent polygons on the CPU from
  the CPU's vertices (GenSorted(), SortPParams()). It takes
  TA_GPU_NO_SORT: a frame whose region array has any sorted pass is
  parsed and sorted on the CPU as ever. Sorting on the GPU is S1's.
- Shadows (the per-triangle renderer's second drawing of polygons in a
  modifier volume): the volumes' triangles are not on the CPU to bound,
  so every shadowed group is drawn again over the whole frame, and the
  stencil limits it to the same pixels.

The decode's outputs gain the translucent draw records (a vertex's worth
plus a header's of 20-byte records) and the translucent polygon words.
The decode's barrier waits for the fragment shaders as well, since the
per-pixel renderer reads those words there. The log gives why frames
were parsed on the CPU, and the pool's counts.

tools/pvr/ta_gpu_test also checks:
- the provoking vertex against the CPU's SetProvokingVertices();
- the translucent draw records and polygon words against the parse;
- the frame made ready to draw against the parse, group by group;
- the overrun verdict at the exact boundary, against the parse run with
  the same room and with one less.

On lavapipe with the validation layer: the T2 corpus byte for byte;
RetroArch screenshots the same with TA Processing CPU and GPU, both
renderers, threaded rendering on and off.
2026-10-15 13:49:07 +02:00
LibretroAdmin 6b41116a31 pvr, rend, maple, libretro: TA limits, punch-through, contexts, guns, RTT
- A vertex with x or y over 1e25 is dropped as upstream (Manic Panic
  Ghosts); one with a negative z is kept and clipped by the GPU (World
  Series Baseball 2K1). The test is on the bits, so fast-math keeps it.
- Punch-through polygons are tested on the shaded alpha (texture x
  vertex colour), not the texel's, in GL, GL4, Vulkan and Vulkan OIT
  (Langrisser Millennium), as upstream.
- Per-strip sorting: sorted translucent polygons write no depth there
  either (Sonic Adventure, which is forced to it, and LOW_END builds).
- A TA context unused for 60 renders is reused, as upstream; counted per
  render started, so frame skip does not change it, and never one of
  another frame size.
- NAOMI 2: the render-end interrupt always 1,500,000 cycles on.
- Light guns: at the gun's line and at vblank-in only SB_MDST is
  cleared; a transfer the game started ends at vblank-out with its answer
  and interrupt (RevolveR), one vblank started gets none (Virtua Cop 2).
  Ninja Assault, Mazan and Gun Survivor get the raw 640x480 position.
  The gun's X follows the widescreen hack when no widescreen cheat is
  on. A "Pointer" controller type aims and fires with one touch and
  reloads with two; the left stick aims on a gamepad in a gun game.
- Render to texture with a clip whose minimum is above 0: the target
  starts at the frame's pixel 0 and the clip is the scissor, in GL and
  Vulkan; only the clip goes back to VRAM, at its place, dithered by the
  pixel's place in the frame. Pixels past max - min were lost, and VRAM
  below the minimum was overwritten.

Save state V28: the gun's transfer owed its end, and the TA contexts'
ages (17 bytes). An older state gets ages 0.
2026-10-14 02:21:54 +02:00
LibretroAdmin 9389db363e rend: stride textures follow the stride; shared modifier volume list
- 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).
2026-10-13 12:26:47 +02:00
LibretroAdmin b04783667e rend: empty clip hang, Vulkan swap chain growth, per-pixel discard, clips
- A render to a texture with FB_X_CLIP or FB_Y_CLIP max < min sized its
  target as a huge number; the power-of-two loop never ended. Such a
  render now draws nothing, as the chip does (GL, GL4, Vulkan, OIT).
- Vulkan: the frontend can lengthen its swap chain while running
  (fast-forward): the rings stay at the size of the start and what is
  kept per frame index grows with it (quad, OSD, framebuffer textures).
- Per-pixel renderers: no early discard for polygons whose source is the
  second accumulator or that have two volumes, and none for "destination
  times white source", which a shadow scales later.
- A polygon clipped inside its tile clip is also kept inside the pixel
  clip (upstream 4fdd28fab, done on pixel rectangles).
- Vulkan sorted triangles: odd triangles of flat strips took the next
  triangle's colour; they are emitted with the provoking vertex first.
- Vulkan: translucent depth mask (Cosmic Smash), as GL has it.
2026-10-13 11:08:04 +02:00
LibretroAdmin 31c2b79f8b rend: palette colours read at their centres, punch-through at the reference, precision
Precision lost by the renderers themselves, not by the chip:

A paletted texture drawn through the palette on the GPU read the 1024
colours at index/1023: on the edge between two of them. Which side it
fell was the GPU's rounding; with Mesa's software renderer 255 of the
1024 came out as their neighbour. They are read at their centres, in the
four sets of shaders. One constant, no work added.

A punch-through texel is kept when its alpha is at least the reference.
Both are 8-bit values, and floats by the time the shader compares them:
a texel equal to the reference could come out just under it and be
dropped (123 of the 65536 pairs with Mesa's software renderer). The
reference is sent half a step low.

OpenGL ES: colours, the fog colour and the samplers were lowp, which is
allowed to be 8 bits with nothing above 2: k/255 on a grid of 1/256, and
a palette index times 255 out of range. They are mediump. Where lowp is
already more, nothing changes.

The fog table's sampler interpolated between entries, and the shader
blends them itself: the neighbouring entry came in twice. Nearest.

The per-pixel OpenGL renderer's 565 render-to-texture target was a
16-bit one, so every blend into it was rounded to 5 or 6 bits. Eight, as
the other renderers have it; it is packed when it is copied to video
memory, as before.

tools/pvr/gles2_test.c: the fog table's sampler as the renderers have it.
2026-10-10 20:09:46 +02:00
LibretroAdmin 70c3d76f46 vulkan: a framebuffer drawn to for the first time starts as the border colour
The OpenGL renderers clear a framebuffer the game has not drawn to lately
to the border colour. The two Vulkan renderers cleared to black. They do
as OpenGL does, on the same frames, and black on the others as before.
2026-10-10 20:09:01 +02:00
LibretroAdmin 72b1b708d2 vk: a replaced quad descriptor set is destroyed after its frames' fences, not after a count of draws
The quad drawer (the kept picture, the VMU and crosshair overlays) makes
a new descriptor set when the image or sampler it draws changes, because
the old one may be in use by frames still on their way through the GPU.
It then kept the old one for 2 x swapchain + 1 more draws and freed it.
A number of draws says nothing about what the GPU has finished: a game
that renders several times to one of the frontend's frames gets through
that many at once.

The old set goes to the command pool the draw's command buffer is of
(CommandPool::DeferDelete, as the per-pixel renderer's buffers already
do with theirs): it is destroyed when the pool comes round to the same
frame slot again, having waited on the way for the fence of every frame
submitted before then - this one included, which can itself have bound
the old set earlier in the same command buffer.

Draw() and VulkanOSD::DrawOSD() take the pool.

Run: tools/threads/live.sh on Vulkan with the validation layer
(VALIDATE=1, per-triangle renderer, the frontend's threaded video too):
passes, no validation error. Virtua Fighter 4 draws the frame it drew.
Not counted: how many replacements those runs contain.
2026-10-08 08:46:04 +02:00
LibretroAdmin d026bcf926 rend: the picture stays on the graphics card when the size changes in place
A change of size that the frontend has room for is made where the
renderer is, and the picture has to come along: a game that draws
part of the screen has the rest from it. It was read back to main
memory at the old size, flipped, and loaded and drawn again at the
new one - the road made for a new context, where nothing else is
left.

Here the context is the same, and the picture never needs to leave
the graphics card:

- OpenGL: the picture is in the frontend's buffer, in the corner
  that was the old size. It is scaled into the corner that is the
  new size, by way of a texture since the two overlap. Two blits.
- Vulkan: the image the last frame was drawn to is not deleted with
  the other old images. The next render starts from it as it does
  from a picture kept across a new context, at whatever size the
  screen now is, and it goes after that frame. Nothing is copied.

With the PowerVR2 filter on, OpenGL keeps the old way: the picture
drawn on is the filter's own, which is made again.

Besides, OpenGL no longer reads the picture back where it could not
be put back (OpenGL ES 2, which has no blit): the read was thrown
away.

Checked under RetroArch with a temporary switch in the core, not in
this patch, that starts at 2x and changes to 1x at frame 280, while
the disc draws only part of the screen: the picture was right after
it in every pass on gl, the per-pixel renderer's first on glcore,
seven passes on vulkan, four of those again under validation, and
the per-pixel renderer's first on vulkan under validation with
synchronization checks. The other way - a larger size inside the
room there is - goes the same code and was not run: the disc's
check of a mipmapped texture fails at 2x.

The headless test passes; the ordinary live test passes on gl as
far as it was run (every pass but the per-pixel renderer's).
2026-10-08 08:18:36 +02:00
LibretroAdmin fcffadcae2 vk: one frame is done with what it shares before the next uses it
The Vulkan renderers draw every frame into the same depth attachment,
and the per-pixel one into the same colour attachment, per-pixel
lists and fragment counter as well. Nothing told Vulkan that one
frame, or one render pass of a frame, had to be done with them before
the next cleared, loaded or reset them. Most drivers keep the order
on their own account. One that does not shows one frame's leavings in
the next: upstream fixed the per-pixel renderer for MoltenVK on macOS
15, which no longer does.

- per-pixel: upstream's dependencies into each subpass from outside
  the render pass, and between the subpasses for the colour
  attachment; barriers round the reset of the fragment counter. One
  more that upstream's layout does not need: the depth attachment is
  stored at the end of the last subpass and changes layout after
  that.
- per-triangle, screen and render to texture: the same for their
  depth attachment, which upstream's change does not reach.
- The clear of the per-pixel lists' image, added three patches ago,
  followed a barrier that made the image ready for shaders rather
  than for the clear. It says the right thing now.

Checked with the validation layer's synchronization checks, which
the live test can now turn on (next patch). Before: hazards in the
first pass of each renderer - the clear after its barrier, the colour
and depth attachments at the start of the render pass, the depth
attachment at its end, and the depth attachment between one frame's
submission and the next. After: none, in the Vulkan passes of the
live test that were run with it - all of both renderers' but the
ones that try the buffer orders, the fullscreen passes included.

Whether any driver in use showed anything for this is not known; the
software one the test runs on does not.
2026-10-08 05:54:51 +02:00
LibretroAdmin 8a4c9a6325 vk: the screen's images outlive a change of size by a few frames
When the output size changes, the Vulkan renderers make new images
for the screen and destroyed the old ones there and then. The last of
the old ones had been handed to the frontend, which shows it again
for as long as it is given nothing newer - and can, before the first
frame at the new size is drawn. From upstream: a crash starting the
240p test suite with the libretro core.

They are put with the things to delete once the frames that could
still use them have gone by, as the renderers' other attachments
already are. Both Vulkan renderers.

The live test passes on vulkan, the per-pixel renderer with the
validation layer on. It has no change of output size in it.
2026-10-08 05:38:20 +02:00
LibretroAdmin d96295b864 vk: all six push constants are pushed
The polygon shaders' push-constant block is six floats. Two places in
the per-triangle renderer pushed five, leaving the last one whatever
it was. From upstream.
2026-10-08 05:17:20 +02:00
LibretroAdmin 39a506b9b1 rend: the screen's picture is kept when the context is destroyed and made again
A game that renders only part of the screen relies on the rest of
the picture staying as it was. Here the rest is in images that
belong to the graphics context, and going fullscreen, or changing a
video setting, destroys the context: the part the game was not
rendering came back black and stayed so until the game drew it again
- which on a loading screen or a menu can be never.

The picture is now fetched when the context is about to go
(rend/last_picture.h), and the first render to the screen in the new
context starts from it. In all four renderers:

- OpenGL, both: read from the framebuffer the frontend gave, and put
  into the one the first render is about to draw in. (OpenGL ES 2
  cannot put it back and is as it was.)
- Vulkan, both: copied from the image the last frame was drawn into,
  made a texture in the new context, and drawn in first where the
  last frame is drawn in for a render in part. The screen's images
  are made to be copied from, without the read-back memory other
  such images get.
- The first render to each framebuffer after a new renderer starts
  from an empty one, as before - unless a picture was kept for it.

One read of the picture for each context destroyed, nothing a frame.
A kept picture of another size is scaled to the new one's.

The test disc renders in part from frame 260 to its end again; the
last patch had it stop at frame 320 so that the fullscreen pass did
not come to depend on when its toggles fell. That pass now sends
three, the later ones while the disc renders in part, and the
per-pixel renderers have a fullscreen pass of their own. The core
says in its log when it keeps a picture, and the passes look for it.

With the putting-back switched off, the fullscreen pass fails on
OpenGL and on Vulkan (tried on the per-triangle renderers). With it,
the live test passes on gl, glcore and vulkan, both renderers each;
the Vulkan passes also with the validation layer on. The headless
test passes.
2026-10-08 03:21:44 +02:00
LibretroAdmin 96c9916c26 vk: a render that covers part of the screen leaves the rest of the picture
A game does not have to render the whole screen: with the pixel clip
or the region array it can render a part, and the console leaves the
rest of the framebuffer as it was. Games do it for loading screens
and for menus where one corner moves.

The Vulkan renderers draw every frame into a fresh image, cleared,
so the part that was not rendered came out black - on the screen for
as long as the game went on rendering in part. (The OpenGL renderers
draw over the last picture and had it right.)

When a render will not cover the screen, the last frame is now drawn
into the new image first, whole, and the render goes over it:

- per-triangle: in the render pass, before the first polygon, with
  the quad pipeline the VMU screens are drawn with.
- per-pixel: in the colour subpass of the frame's first pass, with a
  quad pipeline of its own for that subpass, and what the frame had
  bound is bound again after it.

A render that covers the screen - nearly all of them - does none of
this, and a framebuffer the game has not drawn to lately still
starts empty.

The test disc renders only the left 200 pixels of its screen from
frame 260 to frame 320, with a white polygon drawn nearest over most
of the right-hand side. Everything on the right has to stay as the
last whole frame drew it, and the white polygon must not appear. On
the old code the Vulkan renderers show the right-hand side black.

The live test passes on vulkan, both renderers, with the validation
layer on, and on gl and glcore; the headless test passes.

Not covered: the part of the picture that is kept does not survive
the context being destroyed and made again (fullscreen), on any
renderer - it is black until the game renders it again. That is why
the disc stops rendering in part after frame 320, before the test's
fullscreen pass could look at it.
2026-10-07 20:03:57 +02:00
LibretroAdmin 7c4756e4ac vk: a texture that becomes a render target gets an image that can be rendered to
When a game renders to a texture at an address where the cache
already has a texture of the same size and format - one the game
uploaded there earlier - the Vulkan renderers rendered into that
texture's image as it was. It was made to be sampled and copied to,
not to be rendered to: what happens then is up to the driver, and
since the last patches it could not be copied from either, which is
how a render gets back to video memory. From upstream, where it
shows as a validation error on Virtua Tennis's player list.

A texture records what its image was made for, and the image is made
again when that is not what a render target needs.

The live test passes on vulkan with the validation layer on. The
case itself - an uploaded texture turned render target - is not on
the test disc.
2026-10-07 19:50:52 +02:00
LibretroAdmin 783264f91c libretro: "Enable RTT (Render To Texture) Buffer" is gone, and the table of games that forced it
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.
2026-10-07 19:36:06 +02:00
LibretroAdmin d359c881bb vk: a render to a texture reaches video memory when the game touches it there
The last patch did this for the OpenGL renderers (rend/rtt_watch.h);
this is the same for the two Vulkan ones. What a game renders to a
texture stays in the texture it was drawn into, the pages of video
memory it belongs in are closed, and the first thing to touch them
has the picture fetched and written there as the console packs it.

- core/rend/vulkan/rtt_read.{h,cpp}, used by both renderers: the
  picture is copied to memory this side can read with commands of
  its own, in a command pool of its own - a read can be asked for in
  the middle of a frame being set up - and waited for. Finer than
  the console's, it is first made the console's size on the card.
- The texture a render goes into can be copied from (one more usage
  flag when it is made).
- What is waiting in the memory a new render covers is dealt with
  before that render touches its texture, while the old picture is
  still what it was.
- Before the texture cache is emptied, what is waiting goes to video
  memory; when the renderer goes, it is dropped.
- rend/rtt_watch: the renderer's name for a picture is as wide as a
  pointer, and everything waiting can be asked for at once.

The live test holds the Vulkan renderers to the disc's check again
(the last patch left it out for them). The program accepts either
rounding of the orange it reads back: 32 of 63 for its green, or 31,
which is what Vulkan here makes of a half.

With "Enable RTT (Render To Texture) Buffer" on, all four renderers
still do what they did; with it off, which is the default, all four
now do this. The option and the table of games that force it on have
nothing left to do that this does not, and go next.

The live test passes on gl, glcore and vulkan (both Vulkan renderers,
threaded rendering on and off, states saved and loaded); the headless
test passes. A run with the Vulkan validation layer on went through
the read-back without a message from it.
2026-10-07 18:38:26 +02:00
LibretroAdmin 49ac112e31 vk: a render is drawn with its own matrices, not the last render's
The per-triangle Vulkan renderer took the matrix that scales a
render's coordinates before it had worked that matrix out for the
render: what it used was the one left by the render before, on the
same drawer. For the screen that is last frame's, the same as this
frame's but for the frame after a change of resolution. For renders
to a texture it is the previous render to a texture's - right while
a game has one target, or several of one size, and wrong as soon as
two of different sizes are rendered in turn: each is drawn at the
other's scale.

Found with the test disc, which since the last patch renders a 128
by 128 target and a 64 by 64 one every frame: the first came out
with its blue quarter filling it. The per-pixel Vulkan renderer and
the OpenGL ones take the matrix after working it out and were right.
2026-10-07 18:38:22 +02:00
LibretroAdmin a2ad6ed32b rend: a modifier volume is clipped to the rectangle it was sent under
A polygon sent under a clipping rectangle, asking to be kept inside
it, is. A modifier volume was not: its rectangle was never recorded,
and its shadow fell wherever the volume reached. Upstream: shadows
running out of the baserunner cameras in World Series Baseball 2K1.

The volume now carries the rectangle and mode it was sent under, as a
polygon does, and all four renderers - both OpenGL ones, and Vulkan
per triangle and per pixel - draw it under that scissor. (Kept inside
only; "kept outside" is not done for volumes, here or upstream, which
has this for its two per-triangle renderers.)

The test disc has a new polygon under a volume that covers its width,
the volume sent under a rectangle that is the polygon's left half:
grey on the left, white on the right. With the clipping switched off
the live test fails on the right half (127, not 255). It passes on
gl, glcore and vulkan, per-pixel included.
2026-10-07 15:16:44 +02:00
LibretroAdmin 2d161a2983 rend: the transforms without glm
The renderers used glm for one thing: the three transforms a frame is
drawn with - guest coordinates to clip space, to the viewport, to the
scissor rectangle. Each is a scale and then a move along each axis and
never anything else, and they were built as 4x4 matrices by
multiplying 4x4 matrices together.

core/rend/xform.h, plain C89, keeps a transform as the six numbers
that are not 0 or 1. Putting two together is three multiplications and
three multiply-adds in place of sixty-four multiplications; a point
goes through one with a multiply-add an axis. A shader still gets a
4x4 matrix, written out from the six. The Vulkan uniform and push
constant structures hold plain arrays of the same size and layout.

Same floats: the old class and the new were run side by side on
600,000 random frames - render to texture or not, any clip rectangle,
screen size, scaling, depth scale - and every element of all three
matrices, and a point and a size taken through each, came out the
same. (That needed glm; tools/rend/xform_test.sh, which stays, checks
the transforms against 4x4 matrices multiplied out in full.)

Code: setting the three up is 324 instructions where it was 666, and
measured on its own about thirteen times as fast (20 million set-ups:
1.16 s to 0.09 s). The six objects that used glm are 1458 instructions
smaller together; the one that grew, the Vulkan drawer, grew by which
of vulkan.hpp's exception constructors the compiler chose to emit, not
by this.

glm had also been bringing <cmath> into four files that did not ask
for it; they do now.

The live test passes on gl, glcore and vulkan, per-pixel renderers
included.
2026-10-07 14:23:01 +02:00
LibretroAdmin 49a9b143f0 shadows: per-triangle renderers scale the base and offset colours too
Inside a modifier volume the PowerVR2 scales what a polygon is shaded
with, its base and offset colours, and then combines them with the
texture and fogs the result as usual. The OpenGL and Vulkan
per-triangle renderers darkened the finished pixel instead, with one
quad over the screen. That is the same thing for a modulated texture
and nothing else: it also darkened a decal texture, which the hardware
leaves alone, and the fog on top.

They now do what the per-pixel renderers do, by drawing twice. Once
the volumes are in the stencil buffer, the opaque and punch-through
polygons that take shadows are drawn again with a shader variant that
scales the two colours (pp_Shadowed), into the pixels that are theirs
(depth equal) and in a volume (stencil).

What that costs is kept to where the shadows are. The rectangle of the
screen the pass's volumes cover is taken from their vertices; only
polygons that reach into it are drawn again, and only the part of them
inside it, by scissor. Nothing outside that rectangle can be in a
volume, so this leaves out no pixel that would have changed. A frame
without modifier volumes does nothing new, and the shaders of polygons
drawn the first time are the same as before.

The darkening quad stays, for what cannot be drawn twice: polygons
that are blended, and opaque ones that wrote no depth to be found by.
The pixels drawn again are marked in stencil bit 6 and the quad skips
them; anything not marked is darkened as it was. Where two polygons
are at the very same depth, the mark is also what keeps the one that
was left showing the first time as the one left showing now.

Within a frame of several render passes only the pass's own polygons
are drawn again. Bump-mapped polygons have their offset values scaled
like any others, as in the per-pixel renderers.

rend/shadows.h holds what the two renderers share. tools/pvr's OpenGL
ES 2 test compiles the shadowed variants on an OpenGL ES 2 context and
checks that white comes out grey without a texture and green with a
green decal texture.
2026-10-06 06:42:39 +02:00
LibretroAdmin e34d0028e4 remove the Fog Effects and Volume Modifier core options
Fog and modifier volumes are not enhancements: they are what the
PowerVR2 does, and a game that uses them looks wrong without them, with
no fog where there should be fog and no shadows where there should be
shadows. Both options defaulted to on and there is no reason to offer
"off".

The two options go, and settings.rend.Fog and
settings.rend.ModifierVolumes with them. The OpenGL, OpenGL per-pixel
and Vulkan renderers (both Vulkan drawers) always use the fog mode the
polygon asks for, always keep the fog table up to date, and always draw
the opaque and translucent modifier volumes.

The translated option tables still carry the two entries until they are
next regenerated; they are looked up by the English table's keys, so
they are not shown.
2026-10-06 03:07:31 +02:00
LibretroAdmin 4bd929c953 threaded rendering: make one frame per retro_run, the one non-threaded makes
With Threaded Rendering on, nothing tied the emulation to retro_run. The
emulation thread ran free, held back only by the audio it pushed to the
frontend itself, 512 samples at a time, from its own thread, whenever
that call blocked. retro_run waited up to 100 ms for whatever render
turned up and returned a duplicate frame if none did. A render the game
started while the last one was still being drawn was thrown away. So a
retro_run could last any length, carry a new frame or not, and its audio
arrived some other time from some other thread.

Each retro_run now makes exactly one frame, the same one it makes with
Threaded Rendering off:

  - retro_run wakes the emulation thread, which runs the machine up to
    vblank and goes back to sleep, as dc_run() does in retro_run itself
    when rendering is not threaded;
  - when the game starts a render the emulation thread hands it to the
    libretro thread and sleeps; that thread reads what the frame takes
    from the game's memory (display lists, textures, framebuffer), lets
    the emulation thread go on, and draws. A render to a texture, which
    ends up back in the game's memory, keeps it asleep until it is done;
  - every render is drawn. The emulation thread waits for the one before
    to be finished; none is dropped;
  - at vblank the frame's audio is sent from the libretro thread in one
    batch, together with its video.

The renderers read some PVR registers while they draw, by which time the
emulation has moved on and may have changed them. Each render now
carries a copy of the registers made when the game started it, and the
renderers' files read through that (PVR_REGS_FOR_RENDERER), so they draw
from what non-threaded rendering would have read.

EmuBaton (core/libretro/emu_baton.h) is that handshake. There is no
lock, no sleep and no timeout in it: every change of hands is one atomic
operation and a thread with nothing to do sleeps on an eventcount. A
save state from the frontend's task thread takes the machine between
two frames.

The "synchronous rendering" and "delay frame swapping" options have
nothing left to choose and are hidden.

tools/threads/live.sh runs the test disc for the same 300 frames with
Threaded Rendering on and off and requires the same screenshot, byte
for byte, and the same save state (live_same.py leaves out the host
addresses a state carries). The old threaded path fails that: its state
differs from the non-threaded one. tools/threads/run.sh puts the new
EmuBaton under ThreadSanitizer.
2026-10-05 13:39:02 +02:00
libretroadmin 45bd2f4e59 vulkan: defer attachment deletion instead of WaitIdle on resize
When an RTT/OIT framebuffer attachment must be grown, the renderer called
GetContext()->WaitIdle() before recreating it, stalling the whole device to
ensure no in-flight frame still referenced the old attachment. Replace this
with a per-frame retire list on CommandPool: the old attachment is moved into
a slot-indexed deferred-delete queue and destroyed in BeginFrame once that
slot's fence has signaled (i.e. the GPU has finished the frame that used it),
which is exactly the guarantee the existing N-buffered command sync already
provides. Removes the full-device idle from the depth/color grow paths in the
screen and RTT drawers and from OITDrawer::MakeBuffers.

The OITBuffers pixel-buffer realloc keeps WaitIdle for now: it has no
CommandPool reference and is a cold resolution-change path.
2026-07-06 08:05:22 +02:00