mirror of
https://github.com/libretro/parallel-dc.git
synced 2026-11-10 17:26:21 -10:00
master
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
068183f906 |
vulkan: per-pixel fragment buffer sized from the screen, and a frame that overflows it is drawn in bands
The per-pixel renderer keeps every translucent fragment of a render pass in one buffer, 16 bytes each. It was as large as the "Accumulation Pixel Buffer Size" option said (512 MB by default), cut down to what the device can bind as one storage buffer. A render pass with more fragments than that dropped the ones past the end: the polygons drawn last lost them. Measured on Jet Grind Radio's character select (charsel.state) at 8x (5120x3840), with the count the shaders already keep, read back as below: the frame asks for 12.1 million fragments, 0.62 a pixel. A 512 MB buffer holds 33.5 million, so this frame does not overflow it on a device that binds 512 MB. On one that binds 128 MB (Mesa's software Vulkan, many mobile GPUs) it holds 8.4 million, and the frame loses its last-drawn translucent sprites outright: the MEW / GUM / TAB names at the bottom of the screen are missing. - The buffer is sized when the renderer starts or the screen changes size, never while a game runs: two fragments a pixel of the screen (rend/oit_size.h), at least 32 MB, at most half of the video memory budget (rend/vram_budget.h) that was left. At 1x that is 32 MB; at 8x 629 MB where the device can bind it. - A device that cannot bind that much as one storage buffer (the lesser of maxStorageBufferRange and maxMemoryAllocationSize) gets it in up to four storage buffers of one size, binding 0 an array of four. The shaders find a fragment's buffer by comparing its index with the first's length: a branch every GPU takes the same way where there is one buffer. - Every render pass's count (the shaders count the fragments that find no room too) is copied out after it ends, into host-visible memory for its frame slot. When the slot comes round again its fence has passed, and the CPU reads what that frame asked for. Nothing waits for it. - A frame that asked for more than there is room for is followed by frames drawn in horizontal bands: each band is all of the frame's render passes, clipped to the band by the scissor, with lists that start empty and need the room of one band's fragments. The count goes up at once and down after a second of frames that would have fitted in fewer. Nothing is dropped and nothing is allocated. - The picture's attachment is cleared by the first band and loaded by the others: a render pass variant (@resumed) differs from the usual one in that attachment's load op and initial layout only, so every pipeline works with both. The last band's final render pass covers the whole picture, as the frame's final render pass always has, for what is drawn after it (the VMU screens, the texture copy). A frame with room for its fragments, which is every frame where the buffer fits, is drawn exactly as before, plus a 4-byte copy for each render pass. Three frames back is the earliest a count is seen, so the first frame or two of a scene that overflows still lose fragments. Tested on Mesa's software Vulkan at 8x with the buffer cut to 128 MB (2 bands) and at 2x cut to 8 MB, without SSAA (2 bands) and with 4x SSAA (15 bands): each picture is identical, to the pixel, to the same frame drawn with room to spare. Without bands the 128 MB picture lost the bottom names. |
||
|
|
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. |
||
|
|
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. |
||
|
|
8cae9ee35c |
vk oit: the per-pixel lists are empty when they are made, and the first frame is not red
"vk oit: the first frame after the per-pixel lists are made has its translucent polygons" drew that frame like any other, after emptying the lists with a full-screen draw in the colour subpass. The draw did not empty them: in that first frame every pixel still had a list, starting at whatever fragment was stored first, and the picture came out covered by it - red all over on the test disc, whose first translucent polygon is red. One frame, each time the renderer starts. Before that patch the frame's last step was left out and its picture was nothing in particular, so this is that patch's doing as far as what shows goes: a frame of red in place of a frame of whatever was in the image. It shows for longer than a frame when the game renders only part of the screen: the rest is kept from frame to frame, and stayed red. A context destroyed and made again starts the renderer afresh, so the picture kept across it (rend/last_picture.h) was put back and then covered, on this renderer alone. The image that holds each pixel's list is now cleared to "empty" when it is made ready, with a clear command outside the render pass. The draw in the colour subpass goes, with the pipeline that was made for it and the render pass's dependency of that subpass on itself. Found by the live test once its fullscreen toggles were made to fall while the disc renders in part (the next patch): the first three pictures after each restart were written out, the first was red, and is right with this. The per-pixel Vulkan passes pass with the validation layer on. |
||
|
|
d0eac165fe |
vk oit: the first frame after the per-pixel lists are made has its translucent polygons
The per-pixel Vulkan renderer keeps a list of translucent fragments for every pixel, and empties the lists at the end of each frame. In the first frame after they are made - the renderer starting, a change of size, a context made again - they hold nothing that means anything, and that frame's translucent polygons were left out altogether rather than put into them. A game that draws one frame and then waits showed that frame without them for as long as it waited. From upstream, which names the background of the progress window in Fighting Force 2. In that frame the lists are now emptied in the colour subpass, before the first translucent polygon, and the frame is drawn like any other. The emptying pipeline is made for that subpass as well as the last, and the render pass lets the colour subpass depend on itself, which the barrier after the emptying needs. (That first frame also emptied the lists at its end with the wrong vertex buffer bound - the full-screen quad's was only bound in the part that was skipped.) The live test passes on vulkan with the validation layer on, which takes the per-pixel renderer through its first frame. Nothing checks that frame's picture: not tested beyond that. |
||
|
|
500c2bb2a7 |
vulkan per-pixel: unsorted translucent polygons may write depth
Translucent polygons that the game says are already in order are not put through the per-pixel lists. They are drawn in the colour subpass, in the order sent, each tested against the depth the ones before it wrote. The depth buffer is an input to the shaders in that subpass and was in a read-only layout, in which Vulkan does not allow depth writes: vkCmdDrawIndexed(): depthWriteEnable is VK_TRUE, while the layout (VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL) of the depth aspect of the depth/stencil attachment in the render pass is read only. This is the case "no depth writes in the colour pass where there is nothing to write" left alone, because here there is something to write. A frame that has such polygons is now drawn with render passes whose colour subpass has the depth buffer in the general layout, which allows reading it as an input and writing it. They differ from the usual ones in that layout and nothing else, so the same pipelines and framebuffers serve both. Frames without unsorted translucent polygons, which is every frame of most games, use the read-only layout as before. With the next patch's disc, whose second render pass is pre-sorted, the validation layer reported the error above for every such draw and now reports nothing. |
||
|
|
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.
|
||
|
|
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 |
||
|
|
dce6d1c0ba | vulkan: oit renderer |