34 Commits
Author SHA1 Message Date
libretroadmin 84f3745962 Texture cache: a fixed map, video memory locks and hashes in arenas
The cache's textures are in room made when the renderer starts - twice
as many as the pool has images, no fewer than 1024 - found by a fixed
open-addressing index (rend/tex_map.h, in place of the unordered_map).
A texture never moves; one taken away is made anew in its place. The
locks on video memory, and each page's list of them, are in arenas made
with it (texcache_reserve()), and the pieces' hashes in arenas by size.
Each says once when it runs out and more is made.

The pool has eight 8x8 and four 16x16 textures of each format whatever
their share, which gave none (some 100 KB), and eight 64x64 pictures
rendered to. A line logged is formatted on the stack.

Nine sites come off tools/vulkan/vk_allocs.allowed: the texture cache
allocates nothing during play. tools/rend/tex_map_test.sh checks the map
against a plain list, with ASan and UBSan. In CI.
2026-11-10 06:26:00 +01:00
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 107dca0fdd Vulkan: a change of renderer makes its pipelines off the draw path
A renderer made again while a game runs (per-triangle, per-pixel, compute,
SSAA taken up or let go, another size) hands its pipelines to the worker
as at the start, and retro_run() holds until they are made
(Renderer::Ready()): it shows the picture kept from the old renderer and
does not run the machine, so no frame makes a pipeline or waits for one.
Before the machine has first run since the context was made, it is still
the start, which waits. The core says at unload how many frames that took.

The frames handed to the frontend are images it may copy from as well
(transfer source): the kept picture's were not.

tools/vulkan/vk_draw_path.sh switches renderer at frame 20, frames paced as
a display has them (vk_hdr's VKH_FRAME_MS): nothing made in retro_run, the
game going on after, and the last picture the one the new options draw
from the start.
2026-11-09 21:48:23 +01:00
libretroadmin babc17aced Render to texture: views through a narrower stride made on the card too
A texture read through a stride narrower than it - a part of a render
drawn through TEXT_CONTROL - is made from the render on the card by every
renderer now, not only Vulkan: the stride's columns copied and the rest of
each row cleared, to what zeros are in the texture's format (ClearOpaque():
opaque black in 565, as the read-back has it; Vulkan's was clear).

vk_hdr counts what the core reads back off the card (VKH_READS), and
tools/vulkan/vk_dma.sh holds the DMA disc to 40 reads as gl_dma.sh does.
The disc's view of a render made every frame is a stride texture now: blue
up to the stride and black past it, the same as the read-back's picture,
alpha included.
2026-11-02 02:32:07 +01:00
libretroadmin 78d2c77623 Render to texture: a texture of another shape made from the render on the card
A render still on the graphics card, drawn as a texture of another shape -
fewer lines, through a stride, a part of it - is made into that texture on
the card (CopyRender(): a copy, or a blit where the render is finer), not
fetched into video memory and made from there, which waited for the card
every frame such a render was made again. Its rows have to be the render's
and all it reads inside what was rendered, in the render's format; it stays
watched by write-protection and is made again with the next render there.
Vulkan and OpenGL 3 / OpenGL ES 3 have it; the rest fetch as before. A page
a dropped lock left protected is not protected again.

gl_frames counts reads off the card (GLF_READS). The DMA disc draws the
bottom half of a render made every frame: tools/gl/gl_dma.sh holds it to 40
reads in its run, where reading it back costs 70.
2026-11-02 01:27:40 +01:00
libretroadmin d41ad6c9e5 Vulkan: command buffers and in-flight textures kept in arrays, nothing allocated a frame
The command pool keeps, for each frame's place in the ring, its pool,
fence and the command buffers made from that pool in plain arrays: the
pool reset each time round frees what they recorded, so a frame takes
them again by count, and submits them where they are. What is retired
to a place is a pointer and its deleter in a list, not a shared_ptr
made for each.

The texture cache's textures in flight are a list a place and a bit a
place in each texture, instead of a hash set a place: one test of a bit
on each use, nothing allocated once the lists have grown, and the
textures of a frame in the order it used them. The caches' cleanups
gather what they drop in a few words on the stack.
2026-11-01 17:55:29 +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 7bb490e576 vulkan: render targets come from the texture pool too
TexturePool::Init checked every format from index 5 up against the
texture decoder's native 16-bit formats, the render target's slot
(TARGET, 7) included. A render target's format never matches those, so
it was never usable and every render to a texture made its own image in
the frame that first needed it - mid-game, with both the raster and the
compute renderers. The decoder check is now for the decoder's two formats
only; the target slot keeps its own check (a colour attachment).

The test disc's renders to textures draw the same picture as before
(raster, 300 frames, no validation messages).
2026-10-20 20:18:51 +02:00
LibretroAdmin eb19604d57 vulkan: the compute renderer draws renders to a texture itself
Every render to a texture was left to the raster renderer when Rendering
Method is Compute. Now the kernels draw it, with the same frame words as
a screen frame, into the very texture the per-pixel renderer would have
drawn it in:

- The texture is OITTextureDrawer's: the frame from its first pixel to
  the pixel clip's last, in powers of two, times Render To Texture
  Upscaling, taken from the texture cache the same way (the code that
  takes it is now one function both use, ComputeBegin() and NewFrame()).
  After the render it is locked in video memory and watched the same way
  (rtt_read.h), so the texture cache, the copy to video memory when the
  game first touches it, and what a later CPU write over the render does
  (the texture is made again from video memory, as with the raster
  renderer) are unchanged. Nothing is read back otherwise.
- It is drawn at one sample a pixel, as the per-pixel renderer draws
  renders to a texture whatever SSAA is. With SSAA on, the kernels for one
  sample are made at Init() as well; nothing is compiled while a game runs.
- The frame's words for such a render: only the pixel clip is drawn
  (times the upscaling, as the raster renderer's scissor), the rest of
  the texture is the clear colour as the raster renderer's render pass
  leaves it, and the transform is the raster renderer's viewport's, that
  many times larger.
- The bins are made at Init() for the largest texture rendered to there
  can be (1024 x 1024 at 8x), a few words a coarse tile; each frame clears
  only the part its own tiles use, so a screen frame clears what it did.
- The images rendered to are made with storage usage as well when the
  compute renderer is on (the pool's, TexturePool::SetTargetStorage(), and
  those the drawer makes outside it), so the kernels write them in place:
  no copy. When the device cannot write that format from a compute shader
  such renders still go to the raster renderer, counted under that reason.
- The texture leaves the walk in the layout textures are sampled in, made
  ready for fragment and compute shaders and for the copy to video memory.

The log says how many of the frames drawn were renders to a texture.

tools/rend/tile_dump.cpp writes renders to a texture too (rtt_NNNNN.bin,
TILE_DUMP_RTT, TILE_DUMP_RTT_EVERY), packed as the renderer packs them at
the size of their texture, and tools/rend/tile_test.sh draws and compares
them with the reference like the screen's frames.
2026-10-20 20:18:47 +02:00
LibretroAdmin 069186704a vulkan: a texture debug log, for textures drawn wrong on another machine
Jet Grind Radio's character select shows dark speckles on the floor's
graffiti and the frogs at 1x on an RTX 5090 with the raster renderers,
and not with the compute renderer, which samples the same images with
the same samplers. Lavapipe draws the frame without them, the raster
renderers matching the compute renderer there, so what the two machines
have differently has to be read on the machine that shows it.

A core option, Texture Debug Log (reicast_texture_debug, disabled by
default): five seconds in and then every ten, a line in the log for the
GPU and its precision limits, and one for each texture the frame before
drew - its address, TCW and TSP, size and pixel format (VQ, the mipmap
bit, planar), the image made of it (Vulkan format, levels, from the pool
or its own), how it was last written (staged copy from the CPU, linear
image, the GPU's decode kernel or copy, render target) and the sampler
it is drawn with (filter, addressing, LOD bias, anisotropy, LOD range).
Then each level of the image is read back from the GPU and its checksum
written beside the checksum of the bytes the CPU uploaded into that
level, "ok" or "DIFF".

When the option is off nothing changes but a test of a flag where a
texture is uploaded. When it is on, the read back waits for the GPU and
makes its buffer and command pool each time: a diagnostic only.

Tested on lavapipe in RetroArch with the validation layer: the log at
frame 300 lists the 33 textures of the character select, every level
"ok"; no validation message.
2026-10-16 03:59:52 +02:00
LibretroAdmin b7b4e771ee rend: the texture pool's shares: 1555 and 4444 nearly as many as 565, up to 128 MB
Jet Grind Radio's scenes, loaded mid-game, asked for more 64x64 4444
mipmapped textures, and 256x256 1555 ones, than the pool had made, and
had them made then: the shares had 1555 and 4444 at half of 565's, and
the textures' part of the budget stopped at 64 MB. They are at three
quarters of 565's now, and the part stops at 128 MB (a sixteenth of what
the budget has left, as before: only cards with 2 GB and more to spare
get more than they did).

A desktop OpenGL driver that does not say what video memory there is
(neither GL_NVX_gpu_memory_info nor GL_ATI_meminfo) has
a budget of 512 MB, a guess, and a pool of 32 MB, too small for the same
scenes: the pool is made as for 1.5 GB to spare there, 96 MB.
2026-10-16 02:00:51 +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 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 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 9fcfec2935 vulkan: textures made on the GPU from their bytes in video memory
A texture used to be converted on the CPU, texel by texel, into the
upload ring, and copied from there to its image. Now its bytes are
copied as they are - the texels, a compressed texture's codebook, the
part of the palette a paletted one uses - into a ring of their own, and
the compute kernel of rend/texdecode.h makes the texels from them
straight into the image: no conversion on the CPU, and for most formats
less to copy (a compressed texture is an eighth or a sixteenth of its
texels, a paletted one a quarter or an eighth, YUV half). The texels are
the CPU's to the bit, and the images have the formats they had.

A planar 16-bit texture in a format the image can have its texels in as
they are - 565 always, 1555 as A1R5G5B5, 4444 as B4G4R4A4 with the
view putting the channels back - is not converted at all: its rows are
copied to the image as they are, with the stride as the row length.

The 16- and 8-bit images are written through views of 16- and 8-bit
integers (a mutable format, extended usage, Vulkan 1.1), which needs the
shaderStorageImageExtendedFormats feature: the device is now made with
it when the GPU has it. What a device cannot make an image of, and
everything on a device that is Vulkan 1.0, is converted on the CPU as
before.

What a frame asks for is recorded once, at the end of Process() and
before the frame's draws, in one command buffer of the texture pool's:
one barrier for all the images, the copies, the dispatches a kernel at a
time, one barrier for all of them again. The kernels, the ring (sized
from the video memory budget: 2 to 16 MB a frame), the descriptor pools
and every list are made when the renderer starts, and nothing is
allocated or compiled while the game runs; a frame that asks for more
than there is room for has the rest converted on the CPU. A frame's
piece of the ring and its descriptor sets are used again when its place
comes round, after the texture pool's fence for it, as the upload ring's
are.

A texture made on the GPU and then on the CPU in the same frame, or
going away, or rendered to, has its job taken back. A texture updated
from GetTexture() now takes a command buffer from the pool only when it
records something: one made on the GPU records nothing of its own, and
neither does one that turns out not to be valid, which used to have an
empty command buffer submitted.
2026-10-15 13:48:11 +02:00
LibretroAdmin 936f902ad4 vk: a texture is converted into the memory it is uploaded from, and that memory is made once
A texture on its way to the GPU was converted into the texture cache's
scratch memory, then copied into a staging buffer, then copied to its
image by the GPU. The staging buffer was the texture's own: a Vulkan
buffer and a piece of the heap made with the image, and kept for as long
as the texture lived - a second copy of every texture in host memory.

The texture cache now asks the renderer where to convert into
(BaseTextureCacheData::UploadMemory), and the Vulkan renderer hands it a
piece of an upload ring: a few large buffers that stay mapped, handed out
a piece per upload and taken back a frame at a time, when the frame the
piece was used in has been through the GPU - the life of the command
buffer the copy is recorded in. The texture is converted there and the
copy to the image starts from there. No copy on the CPU, no buffer per
texture, nothing kept once the image has its texels. A buffer of the ring
that has gone unused for some ten seconds is given back.

What is not converted in place still goes through the ring, with the one
copy it had: a texture that is scaled up (the scaler has its own memory)
or dumped, a custom texture, and the renderer's own small textures (fog,
palette, the overlay). A small image that is written where it is, at the
pitch it has, is as before. OpenGL is as before: it has no memory to give
and the cache converts into its own.

Mipmap levels. They are converted one after another from the smallest,
and a copy to an image has to start at a multiple of four bytes, which a
level of two-byte texels after the 1x1 one does not. The levels used to
be copied one by one into the staging buffer to line them up. They are
now converted a few bytes into the piece, which puts every level but the
1x1 one where it has to be, and that level's one texel is copied to the
start: core/rend/upload_layout.h, with tools/rend/upload_layout_test.sh
(texels of 1, 2 and 4 bytes, 1 to 12 levels: every level on a multiple of
four, inside the piece, with the bytes converted for it). The old loop
took every texture that was not 32-bit for a 16-bit one; an 8-bit one
with levels would have been read and written past its end. There is none
today - a paletted texture kept as 8 bits is never mipmapped.

Virtua Fighter 4 to frame 2700, in its attract-mode fight, on Vulkan
(RetroArch from source, Mesa's software Vulkan): 163 uploads, 9.78 MB
converted in place, 1.24 MB still copied. The picture is as it was. (The
ring's buffers were made 14 times in that run, when a buffer went back as
soon as it was unused for one round; they are kept longer since, and that
has not been counted again.)

Tested: tools/threads/headless.sh; tools/threads/live.sh on Vulkan with
the validation layer, per-pixel renderer and fullscreen toggles included,
no message from the layer; the layout test. Builds for Windows.
2026-10-08 19:52:34 +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 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 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 b5da96cd78 vulkan: VMA is gone
The renderer's three allocation sites - textures, framebuffer
attachments and buffers - now go through vk_heap, and vk_mem_alloc.h and
its wrapper are out of the tree. The core is 182 KB smaller.

core/rend/vulkan/heap.{h,cpp} is what the renderer's classes hold: the
Allocation that gives its memory back when it goes, as before, and
VulkanHeap in place of VMAllocator. An allocation's pointer comes with
it, so MapMemory() is a load.

What each kind of memory asks for:

 - textures and attachments: device-local, falling back to whatever the
   image allows; a transient attachment first tries memory the device
   only backs if it has to
 - buffers and images the CPU fills: host-visible and coherent, on the
   device where the device has such memory. VMA was asked for
   host-visible only and nothing ever flushed, which was wrong on a
   device whose host-visible memory is not coherent
 - buffers the CPU reads back (render-to-texture readback): host-visible
   and host-cached. VMA put these in device memory seen through the bus
   on a discrete card, which is write-combined and slow to read

One block for images and one for the CPU's buffers are taken when the
device is set up, so a frame gets memory as an offset rather than from
the driver. A block is 64 MB, or an eighth of the device's memory where
that is less.

tools/threads/live.sh passes on the Vulkan renderer: the screenshot, the
save-state comparison with non-threaded rendering, and save, load and
reset back to back. A run under the Khronos validation layer reports
nothing. Software Vulkan only; not run on a GPU.
2026-10-05 17:32:12 +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
twinaphex 5c3c881c7f Backport https://github.com/flyinghead/flycast/commit/fe17d79e8da08692db31dd9b6610740b0f334a66 2021-04-11 04:26:37 +02:00
twinaphex c8a92b4ea5 Backport https://github.com/flyinghead/flycast/commit/190ab6690ac6bcb5218fba55d7c50fc7954ab65c 2021-03-06 00:43:58 +01:00
barbudreadmon ea1278ee1f (REND) backport https://github.com/flyinghead/flycast/commit/b147f71ffa2472a984a4aa3a37f12482d6d08351 2021-01-19 19:52:20 +01: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 fb83d42d2f renderer: generate mipmaps for custom textures 2020-03-11 17:03:31 +01:00
Flyinghead 988000ca89 vulkan: corrupted textures with mipmap and xBRZ 2020-02-17 17:45:21 +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 b5742d36dc vulkan: copy provoking vertex. crash when rtt2vram. rtt vram stride
setImageLayout was missing TransferSrcOptimal as source layout
Vulkan needs the first vertex to be the provoking vertex so copy 3rd on
1st when flat shading.
VRAM stride was incorrect (RTT to VRAM)

Add 4GB pixel buffer size option
2019-12-10 12:56:58 +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 dce6d1c0ba vulkan: oit renderer 2019-12-03 18:33:55 +01:00
Flyinghead 4592a287eb vulkan renderer 2019-12-03 17:38:51 +01:00