mirror of
https://github.com/libretro/parallel-dc.git
synced 2026-11-10 03:11:26 -10:00
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.
88 lines
3.0 KiB
C++
88 lines
3.0 KiB
C++
/* The GPU time of the screen's renders, for the log: a timestamp before a
|
|
* frame's commands and one after, read back when the frame's slot of the
|
|
* command pool comes round again - its fence has been waited for by then,
|
|
* so the read never waits, and nothing is read before that. Nothing is
|
|
* allocated after Init(): the times are kept in room made there.
|
|
*
|
|
* At Term(), one line: how many frames, and their mean, 99th percentile
|
|
* and longest GPU time. */
|
|
#pragma once
|
|
#include <algorithm>
|
|
#include <vector>
|
|
#include "vulkan_context.h"
|
|
|
|
class GpuFrameTimer
|
|
{
|
|
public:
|
|
void Init(int slots, const char *what)
|
|
{
|
|
Term();
|
|
this->what = what;
|
|
VulkanContext *context = VulkanContext::Instance();
|
|
const std::vector<vk::QueueFamilyProperties> families = context->GetPhysicalDevice().getQueueFamilyProperties();
|
|
const u32 family = context->GetGraphicsQueueFamilyIndex();
|
|
const u32 bits = family < families.size() ? families[family].timestampValidBits : 0;
|
|
if (bits == 0 || slots <= 0)
|
|
return;
|
|
queries = context->GetDevice().createQueryPoolUnique(vk::QueryPoolCreateInfo(vk::QueryPoolCreateFlags(),
|
|
vk::QueryType::eTimestamp, 2 * slots));
|
|
period = context->GetPhysicalDevice().getProperties().limits.timestampPeriod;
|
|
mask = bits >= 64 ? ~0ull : (1ull << bits) - 1;
|
|
written.assign(slots, false);
|
|
times.clear();
|
|
times.reserve(MaxTimes);
|
|
}
|
|
|
|
/* Before the first command of the frame in @slot of the command pool,
|
|
* outside a render pass: the time the slot's last frame took is taken */
|
|
void Begin(vk::CommandBuffer cmd, int slot)
|
|
{
|
|
if (!queries || slot < 0 || slot >= (int)written.size())
|
|
return;
|
|
if (written[slot])
|
|
{
|
|
u64 ts[2];
|
|
if (VulkanContext::Instance()->GetDevice().getQueryPoolResults(*queries, 2 * slot, 2, sizeof(ts), ts, sizeof(u64),
|
|
vk::QueryResultFlagBits::e64) == vk::Result::eSuccess && times.size() < MaxTimes)
|
|
times.push_back((float)((double)((ts[1] - ts[0]) & mask) * period / 1e6));
|
|
}
|
|
cmd.resetQueryPool(*queries, 2 * slot, 2);
|
|
cmd.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe, *queries, 2 * slot);
|
|
written[slot] = true;
|
|
}
|
|
// ...and after its last
|
|
void End(vk::CommandBuffer cmd, int slot)
|
|
{
|
|
if (!queries || slot < 0 || slot >= (int)written.size())
|
|
return;
|
|
cmd.writeTimestamp(vk::PipelineStageFlagBits::eBottomOfPipe, *queries, 2 * slot + 1);
|
|
}
|
|
|
|
void Term()
|
|
{
|
|
if (!times.empty())
|
|
{
|
|
std::vector<float> sorted(times);
|
|
std::sort(sorted.begin(), sorted.end());
|
|
double sum = 0;
|
|
for (float t : sorted)
|
|
sum += t;
|
|
NOTICE_LOG(RENDERER, "Vulkan %s: GPU time of %u frames: mean %.3f ms, 99th percentile %.3f ms, longest %.3f ms", what,
|
|
(u32)sorted.size(), sum / sorted.size(), sorted[(sorted.size() * 99) / 100 < sorted.size() ? (sorted.size() * 99) / 100 : sorted.size() - 1],
|
|
sorted.back());
|
|
}
|
|
times.clear();
|
|
written.clear();
|
|
queries.reset();
|
|
}
|
|
|
|
private:
|
|
static constexpr size_t MaxTimes = 1 << 16;
|
|
vk::UniqueQueryPool queries;
|
|
double period = 0; // ns a tick
|
|
u64 mask = 0;
|
|
std::vector<bool> written;
|
|
std::vector<float> times; // ms
|
|
const char *what = "";
|
|
};
|