mirror of
https://github.com/libretro/parallel-dc.git
synced 2026-11-10 11:21:28 -10:00
master
17
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0d947c9797 |
HDR output: Color Format (24-bit, HDR10, scRGB) and Display Transform
A Color Format core option hands the frontend 30-bit HDR10 or 64-bit scRGB instead of the 24-bit picture, on Vulkan, Direct3D 11 and Direct3D 12 and every renderer of each, through the libretro HDR API (the pixel formats, GET_DISPLAY_METADATA, GET_HDR_METADATA). The picture is placed at the frontend's paper white and mapped onto the display through a Display Transform (beetle-psx's, vendored in rend/hdr/: Reinhard knee by default, which keeps everything below paper white as it was, ACES, ACES 2.0, AgX, PBR Neutral, GT, Hable and others), with optional HDR Expansion. At 24-bit, or when the frontend refuses, none of it runs and the picture is the same to the byte. On Vulkan the encode is the last subpass of the frame's render pass on the rasterizers and one draw on the compute renderer and CPU framebuffers, recorded into the frame's own submission (after the chip's write-out on a frame that has one); on Direct3D 11 and 12 it is one draw in the frame's context or command list, from one shared DXBC blob (tools/rend/kernels_dxbc.sh update-hdr). Nothing is copied and no submission or wait is added. ACES 2.0 (Full)'s cube is baked off the frame by a worker thread handed its jobs through atomics and an eventcount, without locks. tools/vulkan/vk_hdr.sh runs every Vulkan renderer at each Color Format and judges the pictures; tools/hdr/d3d_hdr.sh does the same with the Windows build on Direct3D 11 under wine; tools/vulkan/hdr_hlsl.sh and tools/hdr/hdr_d3d.sh hold the HLSL, and the Direct3D 11 and 12 encoders under wine, to the GLSL over every curve and setting; tools/hdr/hdr_cube.sh holds the worker's cube to the cube baked in place, under TSan and ASan/UBSan. All of them run in the Vulkan negotiation workflow. |
||
|
|
de13a9f222 |
Build on Apple, 32-bit x86 and hosts without a recompiler
What the new CI targets tripped over: - Apple's headers define nil: the unused bitfields named nil in aica.h and modem_regs.h are unnamed now, and the GD-ROM's ByteCount register is GDByteCount (the name clashed there too) - arm64 macOS/iOS/tvOS: the signal context's registers are __ss.__pc and __ss.__x[], and the instruction cache is flushed with sys_icache_invalidate - an ARM build without a recompiler (Apple arm64) still needs the interpreter's do_sqw_nommu_area_3, and bm_RamWriteAccess only exists with a recompiler - 32-bit x86: the prepass alpha static_assert compares both sides as float (x87 otherwise compares the literal at its wider precision) |
||
|
|
d070135cff |
vulkan: the raster renderers' shaders as SPIR-V made with the source
Both raster renderers built every shader they drew with by running glslang in the core, the first time a polygon of its kind came: 15 compiles inside retro_run in Jet Grind Radio's first 24 frames on the per-triangle renderer, 22 on the per-pixel one, and one more each time a game drew something new. The shaders are now text files (core/rend/vulkan/shaders/) and their SPIR-V is made by tools/rend/kernels_spv.sh, as the TA and texture kernels' already is, into core/rend/vulkan/raster_spv.inc, which is committed. A build needs no glslangValidator and the core compiles none of them. `kernels_spv.sh check` (run by tools/pvr/ta_gpu_test.sh and tools/rend/texdecode_test.sh) compares the SHA-1 of each module's text with the one in the file, and with the same glslangValidator every word. The #defines a shader was made from were 16 for a per-triangle polygon and 15 for a per-pixel one: tens of thousands of possible shaders. Most of them are now the bits of one specialization constant (vulkan/pso_key.h). A pipeline is made with its polygon's bits, the device folds them as constants, and what is left is what the #defines made: the same shader, nothing more per pixel. Only what changes what a shader declares is a module of its own: flat or smooth interpolation, SSAA's sample rate (at each sample, once a pixel, none), the per-pixel renderer's pass, a translucent modifier volume's mode, SSAA in the per-pixel shaders' header, and whether the depth prepass's polygon is textured. That is 58 modules in all, about 670 KB of SPIR-V. The depth prepass's flags (PREPASS_*) move to pso_key.h, where its shader reads them too. SSAA's count of samples is a specialization constant too, so the modules do not change with it. Written as code instead of #if, the shaders keep every operation in the order it was. One precision change: in the per-triangle shader the offset colour was used as the lowp input when the pixel was not in a modifier volume, and as a highp copy when it was. It is now the highp copy in both cases. Only GPUs that compute lowp at half precision see any difference, and there it is two products (the per-vertex fog factor and the bump map's angle) now computed at full precision. The shaders' modules are made when a renderer starts (RasterModules): creating a module from words compiles nothing. |
||
|
|
b5a3caa3ae |
vulkan: the depth prepass of sorted translucent polygons, GPU decode too
The prepass of rend/sorter.h for the Vulkan per-triangle renderer, by triangle and by strip, and for frames whose vertices the GPU decodes and whose triangles the CPU sorts (TA_GPU_SORT). Its pipelines are all made with the render pass, never in the middle of a frame: one fragment shader that works out a polygon's alpha as the colour pass's does - the same expressions in the same precisions, with what that one has as macros read from the push constants (where the colour pass has its trilinear alpha, which no polygon of the prepass needs) - made textured or not, flat or Gouraud, and at each SSAA rate; pipelines for each of those and each culling, depth test and write on, no colour writes. The push constants the colour pass left are put back after it. Pipelines made (PipelineManager::CreatePrepassPipelines(), for the screen's and the render-to-texture pass): prepassPipelines[textured] [Gouraud][no culling, front, back][per-sample rate], 12 without SSAA and 24 with, from 4 (8) prepass shaders (ShaderManager::GetPrepassShader()). They are not made again when the way of sorting changes. GPU decode: ta_gpu_prepass(), the sorted passes' triangles grouped as GenPrepass() does, from their headers (no vertex alpha looked at: the vertices are the GPU's), their indices after the sorted ones in the decoder's room, which is twice what it was. |
||
|
|
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. |
||
|
|
6b41116a31 |
pvr, rend, maple, libretro: TA limits, punch-through, contexts, guns, RTT
- A vertex with x or y over 1e25 is dropped as upstream (Manic Panic Ghosts); one with a negative z is kept and clipped by the GPU (World Series Baseball 2K1). The test is on the bits, so fast-math keeps it. - Punch-through polygons are tested on the shaded alpha (texture x vertex colour), not the texel's, in GL, GL4, Vulkan and Vulkan OIT (Langrisser Millennium), as upstream. - Per-strip sorting: sorted translucent polygons write no depth there either (Sonic Adventure, which is forced to it, and LOW_END builds). - A TA context unused for 60 renders is reused, as upstream; counted per render started, so frame skip does not change it, and never one of another frame size. - NAOMI 2: the render-end interrupt always 1,500,000 cycles on. - Light guns: at the gun's line and at vblank-in only SB_MDST is cleared; a transfer the game started ends at vblank-out with its answer and interrupt (RevolveR), one vblank started gets none (Virtua Cop 2). Ninja Assault, Mazan and Gun Survivor get the raw 640x480 position. The gun's X follows the widescreen hack when no widescreen cheat is on. A "Pointer" controller type aims and fires with one touch and reloads with two; the left stick aims on a gamepad in a gun game. - Render to texture with a clip whose minimum is above 0: the target starts at the frame's pixel 0 and the clip is the scissor, in GL and Vulkan; only the clip goes back to VRAM, at its place, dithered by the pixel's place in the frame. Pixels past max - min were lost, and VRAM below the minimum was overwritten. Save state V28: the gun's transfer owed its end, and the TA contexts' ages (17 bytes). An older state gets ages 0. |
||
|
|
31c2b79f8b |
rend: palette colours read at their centres, punch-through at the reference, precision
Precision lost by the renderers themselves, not by the chip: A paletted texture drawn through the palette on the GPU read the 1024 colours at index/1023: on the edge between two of them. Which side it fell was the GPU's rounding; with Mesa's software renderer 255 of the 1024 came out as their neighbour. They are read at their centres, in the four sets of shaders. One constant, no work added. A punch-through texel is kept when its alpha is at least the reference. Both are 8-bit values, and floats by the time the shader compares them: a texel equal to the reference could come out just under it and be dropped (123 of the 65536 pairs with Mesa's software renderer). The reference is sent half a step low. OpenGL ES: colours, the fog colour and the samplers were lowp, which is allowed to be 8 bits with nothing above 2: k/255 on a grid of 1/256, and a palette index times 255 out of range. They are mediump. Where lowp is already more, nothing changes. The fog table's sampler interpolated between entries, and the shader blends them itself: the neighbouring entry came in twice. Nearest. The per-pixel OpenGL renderer's 565 render-to-texture target was a 16-bit one, so every blend into it was rounded to 5 or 6 bits. Eight, as the other renderers have it; it is packed when it is copied to video memory, as before. tools/pvr/gles2_test.c: the fog table's sampler as the renderers have it. |
||
|
|
791f4381eb |
rend: a depth that cannot be taken the logarithm of is clamped
The fragment's depth is written as log2(1 + w). A polygon with a vertex behind the eye can have w below -1 across part of it, the logarithm of a negative number is not a number, and what the depth test does with that is the driver's business. From upstream: the white background of the prison in Rayman 2. w is held at just above -1 before the logarithm, in all four renderers' shaders (six places). |
||
|
|
1e725b9845 |
shaders: texture coordinates and the clip rectangle in high precision
The OpenGL shaders ask for a precision for everything, and on OpenGL ES the driver may take them at their word. Three of the things asked for were too little for what they hold. Texture coordinates were mediump, which may be a 16-bit float with ten bits of mantissa. A coordinate that repeats a 256-texel texture four times has run out of bits to say which texel, and the texture swims or goes blocky. They are highp now, in the vertex and the fragment shader. The Vulkan shaders had the same mediump on them, which there means the driver is allowed 16 bits; it is removed. The clip rectangle, which is in pixels, was a lowp uniform. lowp need not reach beyond 2. It is highp. The colour worked out in the fragment shader - texture times colour, plus offset, into fog - was kept in lowp variables, which may be eight bits and a sign: every step rounded to an eighth bit again. It is kept in mediump and rounded once, on the way out. What comes in, the vertex colours, stays lowp; eight bits is what they are. Desktop OpenGL ignores precision and is not changed by this. The fragment shader already used highp for fog, so nothing new is asked of a driver. All variants compile with glslang (GLSL ES 1.00 and 3.00, GLSL 1.20 and 1.30, Vulkan) and tools/pvr's OpenGL ES 2 test passes. Not run on a device with real low precision, where the difference is. |
||
|
|
49a9b143f0 |
shadows: per-triangle renderers scale the base and offset colours too
Inside a modifier volume the PowerVR2 scales what a polygon is shaded with, its base and offset colours, and then combines them with the texture and fogs the result as usual. The OpenGL and Vulkan per-triangle renderers darkened the finished pixel instead, with one quad over the screen. That is the same thing for a modulated texture and nothing else: it also darkened a decal texture, which the hardware leaves alone, and the fog on top. They now do what the per-pixel renderers do, by drawing twice. Once the volumes are in the stencil buffer, the opaque and punch-through polygons that take shadows are drawn again with a shader variant that scales the two colours (pp_Shadowed), into the pixels that are theirs (depth equal) and in a volume (stencil). What that costs is kept to where the shadows are. The rectangle of the screen the pass's volumes cover is taken from their vertices; only polygons that reach into it are drawn again, and only the part of them inside it, by scissor. Nothing outside that rectangle can be in a volume, so this leaves out no pixel that would have changed. A frame without modifier volumes does nothing new, and the shaders of polygons drawn the first time are the same as before. The darkening quad stays, for what cannot be drawn twice: polygons that are blended, and opaque ones that wrote no depth to be found by. The pixels drawn again are marked in stencil bit 6 and the quad skips them; anything not marked is darkened as it was. Where two polygons are at the very same depth, the mark is also what keeps the one that was left showing the first time as the one left showing now. Within a frame of several render passes only the pass's own polygons are drawn again. Bump-mapped polygons have their offset values scaled like any others, as in the per-pixel renderers. rend/shadows.h holds what the two renderers share. tools/pvr's OpenGL ES 2 test compiles the shadowed variants on an OpenGL ES 2 context and checks that white comes out grey without a texture and green with a green decal texture. |
||
|
|
2f8c9cbb39 |
fog: look-up table mode 2 replaces the colour after texturing
In fog look-up table mode 2 the fog colour takes the place of the pixel's colour and the fog coefficient the place of its alpha. The shaders did that to the polygon's base colour before the texture was combined with it, so a textured polygon in this mode showed its texture, modulated or decal, with the fog colour only underneath. It is a mode of the PowerVR2's fog unit, and the fog unit comes after the unit that combines texture, base and offset. What it replaces is the combined colour: the pixel comes out in plain fog colour, with the coefficient as alpha, whatever the texture is. reicast's reference renderer does it there, and so does Deecy's (unfinished) handling of the mode. For an untextured polygon, the usual case, nothing changes. The four shaders (OpenGL, OpenGL per-pixel, Vulkan, Vulkan per-pixel) now do it at the fog stage, next to the other fog modes. tools/pvr's OpenGL ES 2 test draws a quad with a green texture and blue fog in this mode: every pixel must be blue with the coefficient for its depth as alpha. Master's shaders give green, with the texture's alpha. All fog modes of all four shaders compile with glslang, 68 variants. This goes on the order of the hardware's units and two software renderers. No game is known here that uses the mode on a textured polygon, and it has not been compared with hardware. |
||
|
|
b1949cb4b0 |
fog: the hardware's coefficient and blend, without log2 and pow
The fog coefficient was worked out in the shaders with log2() and pow() for every fogged pixel, and the two table values blended by the texture filter. The PowerVR2 does it in whole numbers: the fog depth, as a float between 1 and 256, gives a table entry from its exponent and the top four bits of its mantissa, the next eight bits blend that entry with the following one out of 256, and the colour is then blended with the fog colour out of 256 as well. fog_mode2() now does exactly that. The float is split by comparing and halving, which is exact, needs no bit operations (so it is the same code under GLSL ES 1.00) and is cheaper than the two transcendental functions it replaces. The two table values are read at their texel centres and blended in the shader. The colour blend uses the hardware's weights, (255 - a) / 256 and a / 256, for the table modes and for per-vertex fog. tools/pvr/fog_test.c compares that arithmetic, in the floats a GPU does it in, with the hardware's for every float between 1 and 256, 67 million of them, with three tables: no difference. The old method came out up to 3/256 away. A depth that is not a number (the depth infinite, the density zero) comes out as the nearest fog depth, 1. The texture lookup the old code ended in happened to swallow it. On OpenGL ES 2 the fog depth was passed to the fragment shader as a varying of depth times density. Varyings are interpolated with perspective correction and depth is not such a quantity: inside a triangle spanning a range of depths it came out too large, by 11% at the middle of one whose depth doubles. The varying is now 1/depth, which that interpolation carries exactly, and the shader divides. The OpenGL, OpenGL per-pixel, Vulkan and Vulkan per-pixel shaders are changed alike. All their variants compile with glslang (GLSL ES 1.00 and 3.00, GLSL 1.20, 1.30, 4.30 and Vulkan); the OpenGL and Vulkan per-triangle renderers have run it. The two per-pixel renderers could not be run here. |
||
|
|
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.
|
||
|
|
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 |
||
|
|
f210bf2b9a | ta: defer index building and strip merging, filter out infinite vertices | ||
|
|
e1b727758f | vulkan: share pixel_buffer_size with gl. Shaders use version 450 | ||
|
|
4592a287eb | vulkan renderer |