Files
libretroadmin b48265ebd6 TA parse: C89 lists and strip indices; Vulkan draws from where it parses
The frame's lists are ta_list.c's (C89): List<T> is a typed facade over
it. A strip's indices are ta_index.c's C89 kernel: one branch-free check
of the whole strip, then a straight run of indices; the per-vertex loop
only where a vertex is bad or the room runs out. The parse is about 20%
faster (2.74 to 2.17 ns a vertex on the heavy disc), its output the same
byte for byte.

With Vulkan on a machine whose video memory the CPU reads back fast (one
memory, as lavapipe's and integrated GPUs'), the vertices and indices
are parsed straight into a slice of memory the card reads, one slice a
frame in flight (rend/parse_arena.h, vulkan/parse_arena_vk.h), and the
translucent sort writes after them: no copy into the main buffer. Other
machines copy as ever.

Tests: tools/pvr/ta_index_test.sh (against the old loop, 1M strips),
tools/rend/parse_arena_test.sh, tools/vulkan/vk_parse_arena.sh (every
render in place, pictures those of the copy, each renderer, threaded,
upscaler, a change of size), tools/pvr/ta_parse_bench.sh; vk_hdr gains
VKH_UNCACHED, VKH_OPTS_AT and VKH_BUFFERS.
2026-11-02 13:11:59 +01:00

470 lines
17 KiB
C++

#define PVR_REGS_FOR_RENDERER // see hw/pvr/pvr_regs.h
/* The compute renderer's frame: see compute_frame.h */
#include "compute_frame.h"
#include "hw/pvr/pvr_regs.h"
#include "hw/pvr/ta_ctx.h"
#include "rend/TexCache.h"
#include "rend/tileclip.h"
#include "rend/depth_scale.h"
#include "rend/ssaa_rate.h"
#include <algorithm>
#include <cmath>
#include <cstring>
static_assert(sizeof(Vertex) == PVRC_VTX_WORDS * 4, "the kernels read the parse's vertices word for word");
static_assert(sizeof(ModTriangle) == 9 * 4, "...and its modifier volume triangles");
// (a record keeps a vertex's number in PVRC_RX_VTX: NAOMI 2's four times the TA data, all vertices)
static_assert(4u * 4 * 1024 * 1024 / sizeof(Vertex) <= PVRC_RX_VTX, "a vertex's number in a record");
bool ComputeFrame::ScalersOff = true;
u32 ComputeFrame::WriteOut(u32 height)
{
if (ScalersOff)
return 0;
/* The Y scaler's filter when the vertical scale factor reduces, then
* the packing to a framebuffer of 16 bits and its dither
* (rend/tile/pvr_scaler.h): its taps a line of the chip's apart, the
* picture's lines over 480 (the internal resolution) */
const u32 lines = std::min(std::max((height + 240) / 480, 1u), 31u);
const u32 fbw = pvr_fb_word(FB_W_CTRL.fb_packmode, FB_W_CTRL.fb_dither, FB_R_CTRL.fb_concat);
if (pvr_scaler_yfilter(SCALER_CTL.full))
return PVRC_FF_YFILTER(lines, Y_COEFF & 0xffff) | PVRC_FF_FBWRITE(0u, fbw);
return fbw != 0 ? PVRC_FF_FBWRITE(lines, fbw) : 0;
}
/* ---- The frame's words ---- */
static u32 machineScale()
{
return ta_data_size() / TA_DATA_SIZE;
}
/* (what TA_context::Alloc() gives each list, the most a frame has) */
u32 ComputeFrame::MaxPrims()
{
return (120 * 1024 + 16384) * machineScale();
}
u32 ComputeFrame::MaxWords()
{
const u32 s = machineScale();
const u32 polys = (16384 + 5120 + 10240) * s;
const u32 mvps = 8192 * s;
const u32 verts = 4 * 1024 * 1024 * s / sizeof(Vertex);
return PVRC_F_WORDS + polys * PVRC_POLY_WORDS + mvps * PVRC_MVP_WORDS + (polys + mvps) * PVRC_SRC_WORDS
+ verts * PVRC_VTX_WORDS + 120 * 1024 * s + 16384 * s * 9;
}
u32 ComputeFrame::RttScale()
{
return settings.rend.RenderToTextureUpscale > 1 ? (u32)settings.rend.RenderToTextureUpscale : 1;
}
/* (a texture rendered to is at most 1024 x 1024, times Render To Texture
* Upscaling's 8 at the most: OITTextureDrawer::GetSize()) */
u32 ComputeFrame::MaxRttTiles()
{
const u32 side = 1024 * 8;
const u32 tiles = (side + (1u << TileLog2(side)) - 1) >> TileLog2(side);
return tiles * tiles;
}
static inline u32 rectWord(int x, int y)
{
return (u32)std::max(x, 0) | ((u32)std::max(y, 0) << 16);
}
static inline u32 fbits(float f)
{
u32 u;
memcpy(&u, &f, 4);
return u;
}
u32 ComputeFrame::Pack(const Layout& layout, Slots& slots, u32 *out, u32 room, u32 *primsOut)
{
const rend_context& rc = pvrrc;
/* A frame of the GPU's TA decode (rc.gpu) has its vertices, indices and
* volume triangles in the decode's buffers, its draw groups' index ranges
* only there, and its 1/fZ_max: the numbering is PVRC_K_GPUSRC's (see
* PVRC_SRC_WORDS), and none of those buffers is copied */
const bool gpu = rc.gpu;
const u32 passCount = (u32)rc.render_passes.used();
why = Why_Passes;
if (passCount == 0 || passCount > PVRC_PASS_MAX)
return 0;
const u32 nop = rc.global_param_op.used(), npt = rc.global_param_pt.used(), ntr = rc.global_param_tr.used();
const u32 nmvo = rc.global_param_mvo.used(), nmvt = rc.global_param_mvo_tr.used();
const u32 polys = nop + npt + ntr, mvps = nmvo + nmvt;
const u32 oPoly = PVRC_F_WORDS;
const u32 oMvp = oPoly + polys * PVRC_POLY_WORDS;
const u32 oSrc = oMvp + mvps * PVRC_MVP_WORDS;
const u32 oVtx = oSrc + (polys + mvps) * PVRC_SRC_WORDS;
const u32 oIdx = gpu ? oVtx : oVtx + rc.verts.used() * PVRC_VTX_WORDS;
const u32 oMod = gpu ? oVtx : oIdx + rc.idx.used();
const u32 total = gpu ? oVtx : oMod + rc.modtrig.used() * 9;
why = Why_Room;
if (total > room)
return 0;
/* Through the X scaler (SCALER_CTL bit 16) the game draws twice as
* wide as the picture, and the kernels draw it so: the matrix, the
* base scissor and the width are twice the picture's across, and each
* pair of pixels drawn is resolved into one of the picture
* (PVRC_FF_HSCALE). A render to a texture keeps the matrix it has,
* which halves what is drawn. The chip's scalers are the core option
* "PVR2 scaler / flicker filter"'s: without it the frame is drawn as
* the raster renderers draw it. */
const bool scalers = !ScalersOff && !rc.isRTT;
const bool hscale = scalers && SCALER_CTL.hscale;
const u32 across = hscale ? 2 : 1;
xform vp = Begin();
vp.sx *= (float)across;
vp.tx *= (float)across;
const u32 drawnWidth = layout.width * across;
const u32 rttScale = rc.isRTT ? std::max(layout.rttScale, 1u) : 1;
// the base scissor: x, y, width, height
int scissor[4];
if (rc.isRTT)
{
/* only the pixel clip is written, as the renderers' renders to a
* texture have it; the texture is cleared around it */
scissor[0] = rc.fb_X_CLIP.min * rttScale;
scissor[1] = rc.fb_Y_CLIP.min * rttScale;
scissor[2] = (rc.fb_X_CLIP.max + 1 - rc.fb_X_CLIP.min) * rttScale;
scissor[3] = (rc.fb_Y_CLIP.max + 1 - rc.fb_Y_CLIP.min) * rttScale;
}
else
{
BaseScissor(scissor[0], scissor[1], scissor[2], scissor[3]);
scissor[0] *= (int)across;
scissor[2] *= (int)across;
}
/* A polygon's clip rectangle (x, y, width, height), as the raster
* renderers' SetTileClip() makes it */
auto tileClip = [&](u32 val, int rect[4]) {
const TileClipping mode = ::GetTileClip(val, vp, rect);
if (mode == TileClipping::Outside)
IntersectClipRect(rect, scissor[0], scissor[1], scissor[2], scissor[3]);
return mode;
};
memset(out, 0, PVRC_F_WORDS * 4);
const int bx0 = std::min(scissor[0], (int)drawnWidth);
const int by0 = std::min(scissor[1], (int)layout.height);
const int bx1 = std::min(scissor[0] + scissor[2], (int)drawnWidth);
const int by1 = std::min(scissor[1] + scissor[3], (int)layout.height);
const u32 baseRect0 = rectWord(bx0, by0), baseRect1 = rectWord(bx1, by1);
/* The numbering, render pass by render pass, and the table of starts.
* (A frame of the decode's: entries of the table where there would be
* numbers, and the third word a volume's count or a group's draw.) */
u32 prim = 0, sources = 0;
auto source = [&](u32 kind, u32 index, u32 third) {
out[oSrc + sources * PVRC_SRC_WORDS] = prim;
out[oSrc + sources * PVRC_SRC_WORDS + 1] = kind | index;
out[oSrc + sources * PVRC_SRC_WORDS + 2] = third;
sources++;
};
auto at = [&]() { return gpu ? sources : prim; };
// the modifier volumes' parameters: where the volume each is in ends
for (u32 m = 0; m < mvps; m++)
{
out[oMvp + m * PVRC_MVP_WORDS + PVRC_M_CLOSE_END] = PVRC_NONE;
out[oMvp + m * PVRC_MVP_WORDS + PVRC_M_CLOSE_BY] = PVRC_NONE;
}
auto volumes = [&](const ModifierVolumeParam *params, u32 first, u32 last, u32 indexBase) {
/* DrawModifierVolumes()'s rule: a parameter in an inclusion or
* exclusion mode ends the volume, which is every triangle since the
* last one that did; parameters without triangles are passed over */
int base = -1;
for (u32 m = first; m < last; m++)
{
const ModifierVolumeParam& p = params[m];
if (p.count == 0)
continue;
source(PVRC_SRC_MODVOL, indexBase + m, p.count);
prim += p.count;
if (base == -1)
base = (int)m;
if (p.isp.DepthMode == 1 || p.isp.DepthMode == 2)
{
// (its last triangle's number; a frame of the decode's: its entry)
const u32 end = gpu ? sources - 1 : prim - 1;
for (u32 k = (u32)base; k <= m; k++)
{
if (params[k].count == 0)
continue;
out[oMvp + (indexBase + k) * PVRC_MVP_WORDS + PVRC_M_CLOSE_END] = end;
out[oMvp + (indexBase + k) * PVRC_MVP_WORDS + PVRC_M_CLOSE_BY] = indexBase + m;
}
base = -1;
}
}
};
auto strips = [&](const List<PolyParam>& list, u32 first, u32 last, u32 indexBase) {
for (u32 i = first; i < last; i++)
{
const PolyParam& pp = list.head()[i];
/* (the decode's groups: count is their vertices, first their
* command's slot, or the translucent's place in the draw records) */
if (pp.count <= 2)
continue;
source(gpu && list.head() == rc.global_param_tr.head() ? PVRC_SRC_TRD : 0, indexBase + i, pp.first);
prim += pp.count - 2;
}
};
RenderPass previous = {};
for (u32 p = 0; p < passCount; p++)
{
const RenderPass& pass = rc.render_passes.head()[p];
u32 *pw = out + PVRC_F_PASS + p * PVRC_PASS_WORDS;
pw[PVRC_P_OP] = at();
strips(rc.global_param_op, previous.op_count, pass.op_count, 0);
pw[PVRC_P_MVO] = at();
volumes(rc.global_param_mvo.head(), previous.mvo_count, pass.mvo_count, 0);
pw[PVRC_P_PT] = at();
strips(rc.global_param_pt, previous.pt_count, pass.pt_count, nop);
pw[PVRC_P_TR] = at();
strips(rc.global_param_tr, previous.tr_count, pass.tr_count, nop + npt);
pw[PVRC_P_TR_END] = at();
if (pass.mv_op_tr_shared)
{
pw[PVRC_P_MVT] = pw[PVRC_P_MVO];
pw[PVRC_P_MVT_END] = pw[PVRC_P_PT];
}
else
{
pw[PVRC_P_MVT] = at();
volumes(rc.global_param_mvo_tr.head(), previous.mvo_tr_count, pass.mvo_tr_count, nmvo);
pw[PVRC_P_MVT_END] = at();
}
pw[PVRC_P_FLAGS] = pass.autosort ? PVRC_PF_AUTOSORT : 0;
previous = pass;
}
// (the decode's frame: its numbering keeps to the room, PVRC_F_PRIM_ROOM)
if (!gpu && (prim > MaxPrims() || ((prim + PVR_CHUNK_PRIMS - 1) >> PVR_CHUNK_LOG2) > layout.chunks))
return 0;
/* The polygon parameters, with what the per-pixel renderer's pipeline
* key and push constants have of them (OITPipelineManager::Key(),
* OITDrawer::DrawPoly()) */
auto poly = [&](const PolyParam& pp, u32 listType, u32 *q) -> bool {
const bool two = pp.tsp1.full != (u32)-1 || pp.tcw1.full != (u32)-1;
u32 f = 0;
if (listType == ListType_Punch_Through)
f |= PVRC_QF_ALPHA_TEST;
if (pp.tcw.PixelFmt == PixelBumpMap)
f |= PVRC_QF_BUMP;
if (pp.tsp.ColorClamp)
f |= PVRC_QF_CLAMP;
if ((pp.tileclip >> 28) == 3)
f |= PVRC_QF_CLIP_INSIDE;
f |= (u32)pp.tsp.FogCtrl << PVRC_QF_FOG_SHIFT;
if (pp.tsp.IgnoreTexA || pp.tcw.PixelFmt == Pixel565)
f |= PVRC_QF_IGNORE_TEXA;
if (pp.pcw.Offset)
f |= PVRC_QF_OFFSET;
f |= (u32)pp.tsp.ShadInstr << PVRC_QF_SHAD_SHIFT;
if (pp.pcw.Texture)
f |= PVRC_QF_TEXTURE;
if (pp.tsp.UseAlpha)
f |= PVRC_QF_USE_ALPHA;
const bool palette = BaseTextureCacheData::IsGpuHandledPaletted(pp.tsp, pp.tcw);
if (palette)
f |= PVRC_QF_PALETTE;
if (pp.pcw.Gouraud)
f |= PVRC_QF_GOURAUD;
if (pp.pcw.Shadow)
f |= PVRC_QF_SHADOW;
if (SsaaSingleRate(pp))
f |= PVRC_QF_SINGLE_RATE;
if (two)
{
f |= PVRC_QF_TWO_VOLUMES;
f |= (u32)pp.tsp1.ShadInstr << PVRC_QF_SHAD1_SHIFT;
f |= (u32)pp.tsp1.FogCtrl << PVRC_QF_FOG1_SHIFT;
if (pp.tsp1.UseAlpha)
f |= PVRC_QF_USE_ALPHA1;
if (pp.tsp1.IgnoreTexA)
f |= PVRC_QF_IGNORE_TEXA1;
if (pp.tsp.IgnoreTexA)
f |= PVRC_QF_IGNORE_TEXA0;
if (pp.tsp.UseAlpha)
f |= PVRC_QF_USE_ALPHA0;
}
u32 slot0 = 0xffff, slot1 = 0xffff;
if (pp.pcw.Texture)
{
slot0 = slots.Slot(pp.texid, pp.tsp);
if (slot0 == PVRC_NONE)
return false;
slot1 = slot0;
if (two && pp.texid1 != (u64)-1)
{
slot1 = slots.Slot(pp.texid1, pp.tsp1);
if (slot1 == PVRC_NONE)
return false;
}
}
float trilinearAlpha = 1.f;
if (pp.tsp.FilterMode > 1 && pp.pcw.Texture && listType != ListType_Punch_Through && pp.tcw.MipMapped == 1)
{
trilinearAlpha = 0.25f * (pp.tsp.MipMapD & 0x3);
if (pp.tsp.FilterMode == 2)
trilinearAlpha = 1.f - trilinearAlpha;
}
u32 pal = 0;
if (palette)
pal = pp.tcw.PixelFmt == PixelPal4 ? (u32)pp.tcw.PalSelect << 4 : (u32)(pp.tcw.PalSelect >> 4) << 8;
int r[4] = {};
const TileClipping clip = tileClip(pp.tileclip, r);
q[PVRC_Q_ISP] = pp.isp.full;
q[PVRC_Q_TSP] = pp.tsp.full;
q[PVRC_Q_TCW] = pp.tcw.full;
q[PVRC_Q_PCW] = pp.pcw.full;
q[PVRC_Q_TSP1] = pp.tsp1.full;
q[PVRC_Q_TCW1] = pp.tcw1.full;
q[PVRC_Q_TEX] = slot0 | (slot1 << 16);
q[PVRC_Q_FLAGS] = f;
q[PVRC_Q_TRILIN] = fbits(trilinearAlpha);
q[PVRC_Q_PAL] = pal;
if (clip == TileClipping::Outside)
{
q[PVRC_Q_RECT] = rectWord(std::min(r[0], bx1), std::min(r[1], by1));
q[PVRC_Q_RECT + 1] = rectWord(std::min(r[0] + r[2], bx1), std::min(r[1] + r[3], by1));
}
else
{
q[PVRC_Q_RECT] = baseRect0;
q[PVRC_Q_RECT + 1] = baseRect1;
}
q[PVRC_Q_CLIP] = rectWord(std::min(r[0], 0xffff), std::min(r[1], 0xffff));
q[PVRC_Q_CLIP + 1] = rectWord(std::min(r[0] + r[2], 0xffff), std::min(r[1] + r[3], 0xffff));
q[PVRC_Q_FIRST] = pp.first;
q[PVRC_Q_LIST] = listType;
return true;
};
why = Why_Textures;
for (u32 i = 0; i < nop; i++)
if (!poly(rc.global_param_op.head()[i], ListType_Opaque, out + oPoly + i * PVRC_POLY_WORDS))
return 0;
for (u32 i = 0; i < npt; i++)
if (!poly(rc.global_param_pt.head()[i], ListType_Punch_Through, out + oPoly + (nop + i) * PVRC_POLY_WORDS))
return 0;
for (u32 i = 0; i < ntr; i++)
if (!poly(rc.global_param_tr.head()[i], ListType_Translucent, out + oPoly + (nop + npt + i) * PVRC_POLY_WORDS))
return 0;
auto mvp = [&](const ModifierVolumeParam& p, u32 *m) {
int r[4] = {};
m[PVRC_M_ISP] = p.isp.full;
if (tileClip(p.tileclip, r) == TileClipping::Outside)
{
m[PVRC_M_RECT] = rectWord(std::min(r[0], bx1), std::min(r[1], by1));
m[PVRC_M_RECT + 1] = rectWord(std::min(r[0] + r[2], bx1), std::min(r[1] + r[3], by1));
}
else
{
m[PVRC_M_RECT] = baseRect0;
m[PVRC_M_RECT + 1] = baseRect1;
}
m[PVRC_M_FIRST] = p.first;
};
for (u32 i = 0; i < nmvo; i++)
mvp(rc.global_param_mvo.head()[i], out + oMvp + i * PVRC_MVP_WORDS);
for (u32 i = 0; i < nmvt; i++)
mvp(rc.global_param_mvo_tr.head()[i], out + oMvp + (nmvo + i) * PVRC_MVP_WORDS);
/* The header */
const u32 tilesX = (drawnWidth + (1u << layout.tileLog2) - 1) >> layout.tileLog2;
const u32 tilesY = (layout.height + (1u << layout.tileLog2) - 1) >> layout.tileLog2;
// (twice the tiles across: the bins have room for a texture rendered to's, more than that)
why = Why_Room;
if ((u64)tilesX * tilesY * layout.chunks > layout.runs)
return 0;
out[PVRC_F_WIDTH] = drawnWidth;
out[PVRC_F_HEIGHT] = layout.height;
out[PVRC_F_TILES_X] = tilesX;
out[PVRC_F_TILES_Y] = tilesY;
out[PVRC_F_TILE_LOG2] = layout.tileLog2;
out[PVRC_F_PRIMS] = gpu ? 0 : prim;
out[PVRC_F_PASSES] = passCount;
out[PVRC_F_FLAGS] = (layout.lastPicture ? (layout.lastScaled ? PVRC_FF_LAST_PICTURE | PVRC_FF_LAST_SCALED : PVRC_FF_LAST_PICTURE) : 0)
| (hscale ? PVRC_FF_HSCALE : 0);
if (scalers)
out[PVRC_F_FLAGS] |= WriteOut(layout.height);
/* A framebuffer the game has not drawn to lately starts as the border
* colour; any other black (OITDrawer::Draw()), a texture rendered to too */
out[PVRC_F_CLEAR] = rc.clearFramebuffer
? (VO_BORDER_COL.Red | (VO_BORDER_COL.Green << 8) | (VO_BORDER_COL.Blue << 16) | 0xff000000u)
: 0xff000000u;
out[PVRC_F_SCISSOR] = (u32)bx0;
out[PVRC_F_SCISSOR + 1] = (u32)by0;
out[PVRC_F_SCISSOR + 2] = (u32)bx1;
out[PVRC_F_SCISSOR + 3] = (u32)by1;
/* (a render to a texture: the matrix is to the frame's own pixels, and
* the raster renderer's viewport is that many times larger) */
const float vs = (float)rttScale;
out[PVRC_F_XFORM] = fbits(vp.sx * vs);
out[PVRC_F_XFORM + 1] = fbits(vp.sy * vs);
out[PVRC_F_XFORM + 2] = fbits(vp.tx * vs);
out[PVRC_F_XFORM + 3] = fbits(vp.ty * vs);
out[PVRC_F_XFORM + 4] = fbits(vp.sz);
/* Where the raster renderers clip a triangle with a vertex behind the
* eye, which the kernels clip there too (pvr_compute.h,
* rec_clip_plane()): their depth scale, over the kernels' 1/w scale */
out[PVRC_F_ZCLIP] = fbits(gpu ? 1.f / vp.sz : pvr_depth_scale(rc.fZ_max) * (1.f / vp.sz));
out[PVRC_F_GPU] = gpu ? (u32)rc.idx.capacity() : 0;
out[PVRC_F_PRIM_ROOM] = MaxPrims();
/* the raster renderers' fragment constants (Vulkan's
* MakeFragmentUniforms(), Direct3D 12's MakeFragmentConstants()) */
{
const u8 *fogVert = (const u8 *)&FOG_COL_VERT; // (BGRA)
const u8 *fogRam = (const u8 *)&FOG_COL_RAM;
for (int c = 0; c < 3; c++)
{
out[PVRC_F_FOG_RAM + c] = fbits(fogRam[2 - c] / 255.0f);
out[PVRC_F_FOG_VERT + c] = fbits(fogVert[2 - c] / 255.0f);
}
const u8 *density = (const u8 *)&FOG_DENSITY;
out[PVRC_F_FOG_DEN] = fbits(density[1] / 128.0f * powf(2.0f, (float)(s8)density[0]));
const int shifts[4] = { 16, 8, 0, 24 };
for (int c = 0; c < 4; c++)
{
out[PVRC_F_CLAMP_MIN + c] = fbits(((rc.fog_clamp_min >> shifts[c]) & 0xFF) / 255.0f);
out[PVRC_F_CLAMP_MAX + c] = fbits(((rc.fog_clamp_max >> shifts[c]) & 0xFF) / 255.0f);
}
// (half a step under the reference: see the same in gles/gles.cpp)
out[PVRC_F_ALPHA_REF] = fbits(((PT_ALPHA_REF & 0xFF) - 0.5f) / 255.0f);
}
out[PVRC_F_SHADE] = fbits(FPU_SHAD_SCALE.scale_factor / 256.f);
out[PVRC_F_CHUNKS] = gpu ? 0 : (prim + PVR_CHUNK_PRIMS - 1) >> PVR_CHUNK_LOG2;
out[PVRC_F_SOURCES] = sources;
out[PVRC_F_POOL] = layout.pool;
out[PVRC_F_O_POLY] = oPoly;
out[PVRC_F_O_MVP] = oMvp;
out[PVRC_F_O_SRC] = oSrc;
out[PVRC_F_O_VTX] = gpu ? 0 : oVtx;
out[PVRC_F_O_IDX] = gpu ? 0 : oIdx;
out[PVRC_F_O_MOD] = gpu ? 0 : oMod;
out[PVRC_F_RUNS] = layout.runs;
MakeFogTexture((u8 *)(out + PVRC_F_FOG));
/* The parse's buffers, with flat shading's colours where the raster
* renderers put them: only now, when nothing can leave the frame to
* them any more, for they change the vertices as well */
if (!gpu)
{
ProvokingVertices();
memcpy(out + oVtx, rc.verts.head(), rc.verts.used() * sizeof(Vertex));
memcpy(out + oIdx, rc.idx.head(), rc.idx.used() * 4);
memcpy(out + oMod, rc.modtrig.head(), rc.modtrig.used() * sizeof(ModTriangle));
}
*primsOut = gpu ? 0 : prim;
why = Why_None;
return total;
}