25 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 b01ee57541 Samplers, texture sets and the sort's room made before the first frame
Every sampler a TSP can ask for - filtered or not, each axis clamped,
mirrored or repeated, the 16 mipmap biases, 288 - is made at the
Vulkan renderer's start into a plain table (SamplerManager::Init()), in
place of a std::map filled as polygons first asked. A drawer's texture
descriptor cache has its tables and a batch of sets with the drawer. The
translucent sort's room is reserved before any renderer's first frame,
the translucent list's polygons and 64K triangles (SortReserve()), and
each renderer's lists of the passes' draws (SortPassesReserve()): a
frame up to them allocates nothing.

Six sites come off the list of allocations during play, 26 left.
2026-11-10 03:25:22 +01:00
libretroadmin daa88b139f Vulkan: nothing made on the draw path
The start waits, on the pipeline thread, for every fixed pipeline the
first frame needs, so no frame makes one or waits for one. A device
without VK_EXT_extended_dynamic_state or _state3 has the general pipeline
of every fixed state a draw can reach made then too: Prepare() runs Key()
over the drawer's draws and every value of the fields it reads. Where they
are too many for the start (neither extension), those of earlier runs are
made, until the blend is folded. HDR output's post effects are made with
it, not in the frame they first turn on.

The quads' pipelines are dropped before the drawers' render passes go: the
pipeline thread could make one with a render pass already destroyed.

tools/vulkan/vk_draw_path.sh runs every renderer on every test disc cold,
without each dynamic state and with HDR output, and fails on a pipeline
made in retro_run (CI). VKH_HIDE_EXT takes a list. The Direct3D tests give
each test disc its own directory: their tracks share names, and every
disc ran the last one made.
2026-11-09 19:51:08 +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 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 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 2cec1266a1 vulkan: the frontend lets go of the picture before a renderer is taken down
Changing the Rendering Method (rasterizer <-> compute), the alpha
sorting or anything else that has the renderer made again while a game
runs destroyed the old renderer's pictures - the images and views last
handed to the frontend with set_image - while the frontend could still
draw them. RetroArch draws the last picture again for as long as no new
one comes (paused, behind its menu, on a frame the game draws nothing
in), and with threaded video its video thread draws frames the core
thread has already moved past. The renderer's Term() only called
wait_sync_index, which waits for the index the core draws into next and
is a no-op in RetroArch's own driver. RetroArch's video thread then
built its filter chain's descriptors from a destroyed image view: an
access violation in the NVIDIA driver under slang_pass_build_commands,
reported on Windows with threaded video on.

Before the renderer destroys anything, the frontend is now told to let
go of its picture with set_image(NULL) - RetroArch's threaded video
draws what it has queued and waits for its own submissions then - and
the queue the frontend and core share is waited idle, under the queue
lock, for a frontend that only forgets the image. It happens only when a
renderer is taken down; no frame waits.

On lavapipe with the validation layer and threaded video, switching the
Rendering Method while running and while paused, Jet Grind Radio: before,
"vkUpdateDescriptorSets(): Invalid VkImageView" from RetroArch's filter
chain and a crash at the seventh switch; after, twelve switches with no
validation error.
2026-10-24 10:18:42 +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 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 5274546c9d vulkan: memory a smaller render size no longer needs goes back to the driver
The heap takes device memory in 64 MB blocks and gave an empty block
back only when an allocation found no room anywhere. Turn the internal
resolution up and down again and the blocks the large images were in
stayed with the core for the rest of the session, empty.

Thirty-two frames after the renderer changes size - by when the frames
that used the old images are long done with, and the images freed (two
frames, with Mesa's software Vulkan) - the blocks that are empty are
given back, all but the last of each kind (memory type; linear or not),
so that the next allocation of every kind is still an offset and not a
trip to the driver. vk_heap_trim_spare() is the new call; once, not every
frame: a block that comes empty in ordinary running is left alone.

Run in RetroArch on Mesa's software Vulkan with the test disc, the render
size taken 1x, 4x, 1x, 4x, 1x, 2x by a line of test code since removed:

  per-triangle  192 MB reserved at 4x; back at 1x, 14 MB in use and 192
                reserved before, "64 MB given back, 128 MB reserved" now,
                both times
  per-pixel     384 MB at 4x; back at 1x 320 MB, both times (its pixel
                buffer stays at the size it grew to)

and the picture at the end is the same file with the trim and without, in
both renderers.

tools/vkheap: the three empty blocks beside one in use go back; with
none in use one of each kind stays, and the next allocation does not ask
the driver.

(Throughput item 4 of the audit, the part of it that can be seen here.
Its budgets per memory heap and a smaller first reservation - 128 MB are
taken at the start for the 14 to 22 MB this disc uses - are not in this.)
2026-10-09 07:16:39 +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 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 a2d967c7ed vulkan: wait for the core's own frames before a renderer is torn down or resized
When a Vulkan renderer is taken down, or resized, it destroys what it
drew with: framebuffers and their images, pipelines, descriptor sets,
the overlays, the command pools themselves. Before taking itself down
it called context->WaitIdle(), which is the frontend's wait_sync_index
and waits for the frontend's frames; RetroArch's does nothing at all.
Before a resize it waited for nothing. The frames that use those things
are the core's own, which it submits itself, and the last one or two
can still be on the GPU.

CommandPool::WaitAll() waits on the fences of everything submitted
through the pool, and both renderers call it first thing in Term() and
Resize(). It gives up after a second, so a frame begun and never
submitted cannot hang it.

These are not per-frame paths: a renderer is torn down when the
per-pixel option is switched or the frontend recreates its context,
and resized when the internal resolution changes.

Not reproduced here. The test setup has no way to change a core option
while running, and the teardown it can cause, a fullscreen toggle, is
one where RetroArch has already let the device go idle. That toggle,
three times over under the validation layer on both renderers, is
clean and does not hang with the wait in.
2026-10-06 07:03:56 +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
Flyinghead c2f735809c handle some palette textures on the GPU. vulkan: some linear tiling
get rid of PixerPacker templates
gl4: no need for GLSL precision qualifier in GL4
vk: Refactor base and oit renderers to avoid code dup
vk: Linear tiling is faster for small, frequently updated textures
Fix Hello Kitty crash with empty first TR polygon
Fix Test Drive Le Mans crash on invalid textures
2020-07-11 19:19:58 +02:00
Flyinghead b018b13cfd recreate the gl/vk texture when loading a custom one 2020-02-24 22:50:35 +01:00
Flyinghead 60a4cf60d1 upload all texture mipmap levels to gpu
implemented for gl/gl4/vulkan/oit
don't auto-generate mipmaps in vulkan
simpler/smaller detwiddle table
2020-02-07 17:39:05 +01:00
Flyinghead f268376547 vulkan: VMU screens and lightgun crosshairs support 2020-01-07 16:05:43 +01:00
flyinghead 82bb310f86 vulkan: workaround for Intel driver bug when depth write is disabled
https://forums.intel.com/s/question/0D50P00004Xwlb0SAB/vulkan-depth-test-fails-when-depth-buffer-write-is-disabled-and-glfragdepth-is-updated-in-fragment-shader
2019-12-22 18:25:26 +01:00
Flyinghead bc61520174 vulkan: delay destroying in-flight images
Keep in-use imave and image views when the texture is updated.
Wait until the graphics queue is idle before changing shared buffers and
attachments.
Refactoring
2019-12-09 21:16:16 +01:00
Flyinghead 66f723acef vulkan: allocate oit buffers in device-local mem. NVidia fixes
BufferData was always allocated in host visible mem, regardless of the
mem property argument.
NVidia gpus don't support blitting to 565 and 5551, which is needed for
mipmaps, so convert to 8888 instead.
Use optimal tiling for oit pointers image (linear not supported by
nvidia and probably slower anyway)
oit screen drawer uses multiple final attachments
2019-12-04 15:14:18 +01:00
Flyinghead 4592a287eb vulkan renderer 2019-12-03 17:38:51 +01:00