mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 18:01:03 +00:00
TOTK_FPS_LIMIT (fps_limit in totk.ini, live, menu row on the Game tab) replaces the 30/60 choice of TOTK_FPS60. The pacer holds frames to the limit in nanoseconds instead of 60 Hz ticks; the real-time step has no lower bound, so game speed stays 1.000 above 60. `display` follows the exact refresh rate of the window's monitor (QueryDisplayConfig), measured again when the window moves or the display mode changes. A limit of 30 switches the patch set fps60 off as before; unset, the older fps60 = 0 still means 30. Above 60 needs the real-time step and is marked experimental. The vsync event stays at 60 Hz: on 1.4.3 the title runs at 144 fps with it, so the game does not wait for it each frame. Field on machine A: 69-88 fps, CPU-bound (GPU 3.4 ms per frame). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
711 lines
32 KiB
C
711 lines
32 KiB
C
// NVN front end, second slice: descriptor pools, textures, samplers, programs,
|
|
// windows with present pacing, and the state objects. Ported from MacNativeGames
|
|
// games/totk/third_party/d2r_gfx/gfx/nvn/hle_nvn.c @ b75be8cf (MIT, Copyright (c) 2026 jhaizhou; origin d2r-mobile),
|
|
// lines 254-535, 1775-1829, 2070-2215 and 2579-2727, and nvn_pace.h; Metal, mach, the translation thread, frame
|
|
// interpolation and Apple diagnostics dropped. Calls that reached Metal go to gfx_backend() (gfx.h).
|
|
//
|
|
// Objects are stored in place in the NVN object memory the game provides (nvn_obj.h); every argument is read from the
|
|
// AAPCS64 registers (host.h arg_x / arg_s / arg_p) and every result returned with ret_x. Command-buffer procs are in
|
|
// nvn_api_cmd.c, the TOTK extension procs in nvn_ext.c.
|
|
#include <windows.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "gfx.h"
|
|
#include "host.h"
|
|
#include "nvn_api.h"
|
|
#include "nvn_hmap.h"
|
|
#include "nvn_tex.h"
|
|
|
|
// ---------- Descriptor pools
|
|
// NVNboolean nvnTexturePoolInitialize(NVNtexturePool *tp, const NVNmemoryPool *p, ptrdiff_t offset, int n)
|
|
NVN_API(nvnTexturePoolInitialize) {
|
|
*(dpool_t *)arg_p(c, 0) = (dpool_t){arg_p(c, 1), (int64_t)arg_x(c, 2), (int32_t)arg_x(c, 3)};
|
|
NVN_LOG("TexturePool n=%d", (int)arg_x(c, 3));
|
|
ret_x(c, 1);
|
|
}
|
|
// void nvnTexturePoolFinalize(NVNtexturePool *tp)
|
|
NVN_API(nvnTexturePoolFinalize) { (void)c; }
|
|
// void nvnTexturePoolRegisterTexture(const NVNtexturePool *tp, int id, const NVNtexture *t, const NVNtextureView *v)
|
|
NVN_API(nvnTexturePoolRegisterTexture) {
|
|
gfx_backend()->texture_register((int)arg_x(c, 1), arg_p(c, 2), arg_p(c, 3));
|
|
}
|
|
// NVNboolean nvnSamplerPoolInitialize(NVNsamplerPool *sp, const NVNmemoryPool *p, ptrdiff_t offset, int n)
|
|
NVN_API(nvnSamplerPoolInitialize) {
|
|
*(dpool_t *)arg_p(c, 0) = (dpool_t){arg_p(c, 1), (int64_t)arg_x(c, 2), (int32_t)arg_x(c, 3)};
|
|
NVN_LOG("SamplerPool n=%d", (int)arg_x(c, 3));
|
|
ret_x(c, 1);
|
|
}
|
|
// void nvnSamplerPoolFinalize(NVNsamplerPool *sp)
|
|
NVN_API(nvnSamplerPoolFinalize) { (void)c; }
|
|
// void nvnSamplerPoolRegisterSampler(const NVNsamplerPool *sp, int id, const NVNsampler *s)
|
|
NVN_API(nvnSamplerPoolRegisterSampler) {
|
|
gfx_backend()->sampler_register((int)arg_x(c, 1), &((const samp_t *)arg_p(c, 2))->p);
|
|
}
|
|
|
|
// ---------- Texture builder
|
|
#define TB(c) ((tex_builder_t *)arg_p(c, 0))
|
|
// void nvnTextureBuilderSetDefaults(NVNtextureBuilder *b): keeps the device
|
|
NVN_API(nvnTextureBuilderSetDefaults) {
|
|
tex_builder_t *b = TB(c);
|
|
NVNdevice *dev = b->dev;
|
|
memset(b, 0, sizeof *b);
|
|
b->dev = dev;
|
|
b->d.swizzle_r = 2;
|
|
b->d.swizzle_g = 3;
|
|
b->d.swizzle_b = 4;
|
|
b->d.swizzle_a = 5;
|
|
b->d.target = NVN_TEXTURE_TARGET_2D;
|
|
b->d.width = b->d.height = b->d.depth = b->d.levels = 1;
|
|
}
|
|
NVN_API(nvnTextureBuilderSetDevice) { TB(c)->dev = arg_p(c, 1); }
|
|
NVN_API(nvnTextureBuilderSetFlags) { TB(c)->d.flags = (int)arg_x(c, 1); }
|
|
NVN_API(nvnTextureBuilderSetTarget) { TB(c)->d.target = (int)arg_x(c, 1); }
|
|
NVN_API(nvnTextureBuilderSetWidth) { TB(c)->d.width = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnTextureBuilderSetHeight) { TB(c)->d.height = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnTextureBuilderSetDepth) { TB(c)->d.depth = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnTextureBuilderSetSize1D) {
|
|
tex_builder_t *b = TB(c);
|
|
b->d.width = (uint32_t)arg_x(c, 1);
|
|
b->d.height = b->d.depth = 1;
|
|
}
|
|
NVN_API(nvnTextureBuilderSetSize2D) {
|
|
tex_builder_t *b = TB(c);
|
|
b->d.width = (uint32_t)arg_x(c, 1);
|
|
b->d.height = (uint32_t)arg_x(c, 2);
|
|
b->d.depth = 1;
|
|
}
|
|
NVN_API(nvnTextureBuilderSetSize3D) {
|
|
tex_builder_t *b = TB(c);
|
|
b->d.width = (uint32_t)arg_x(c, 1);
|
|
b->d.height = (uint32_t)arg_x(c, 2);
|
|
b->d.depth = (uint32_t)arg_x(c, 3);
|
|
}
|
|
NVN_API(nvnTextureBuilderSetLevels) { TB(c)->d.levels = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnTextureBuilderSetFormat) { TB(c)->d.format = (int)arg_x(c, 1); }
|
|
// The back end samples the depth plane; stencil-plane sampling stops.
|
|
NVN_API(nvnTextureBuilderSetDepthStencilMode) {
|
|
if ((uint32_t)arg_x(c, 1) != NVN_TEXTURE_DEPTH_STENCIL_MODE_DEPTH)
|
|
rt_fatal("nvn: stencil-plane texture sampling is not implemented");
|
|
}
|
|
NVN_API(nvnTextureBuilderSetSwizzle) {
|
|
uint32_t s[4] = {(uint32_t)arg_x(c, 1), (uint32_t)arg_x(c, 2), (uint32_t)arg_x(c, 3), (uint32_t)arg_x(c, 4)};
|
|
for (int i = 0; i < 4; i++)
|
|
if (s[i] > 5) rt_fatal("nvn: invalid texture swizzle %u", s[i]);
|
|
nvn_texdesc *d = &TB(c)->d;
|
|
d->swizzle_r = s[0];
|
|
d->swizzle_g = s[1];
|
|
d->swizzle_b = s[2];
|
|
d->swizzle_a = s[3];
|
|
}
|
|
NVN_API(nvnTextureBuilderSetSamples) { TB(c)->d.samples = (int)arg_x(c, 1); }
|
|
NVN_API(nvnTextureBuilderSetStride) { TB(c)->d.stride = (uint32_t)arg_x(c, 1); }
|
|
// void nvnTextureBuilderSetStorage(NVNtextureBuilder *b, NVNmemoryPool *p, ptrdiff_t offset)
|
|
NVN_API(nvnTextureBuilderSetStorage) {
|
|
tex_builder_t *b = TB(c);
|
|
b->pool = arg_p(c, 1);
|
|
b->off = (int64_t)arg_x(c, 2);
|
|
}
|
|
// void nvnTextureBuilderSetPackagedTextureLayout(NVNtextureBuilder *b, const NVNpackagedTextureLayout *l)
|
|
NVN_API(nvnTextureBuilderSetPackagedTextureLayout) {
|
|
tex_builder_t *b = TB(c);
|
|
const NVNpackagedTextureLayout *l = arg_p(c, 1);
|
|
b->has_layout = l != NULL;
|
|
if (l) memcpy(b->layout, l->layout, 8);
|
|
b->d.pkg_block_h = l ? l->layout[0] + 1 : 0;
|
|
}
|
|
// void nvnTextureBuilderSetPackagedTextureData(NVNtextureBuilder *b, const void *data): packaged (already GPU-laid-out)
|
|
// data, copied into the texture's pool storage at Initialize
|
|
NVN_API(nvnTextureBuilderSetPackagedTextureData) { TB(c)->pkg_data = arg_p(c, 1); }
|
|
NVN_API(nvnTextureBuilderGetPackagedTextureData) { ret_x(c, (uintptr_t)TB(c)->pkg_data); }
|
|
// Storage classes are opaque compatibility identifiers: all texture storage lives in byte-addressed pools and is decoded
|
|
// per texture description, so class 1 names that convention; buffer textures use class 0. Not a Tegra memory kind.
|
|
static uint64_t storage_class(const nvn_texdesc *d) { return d->target == NVN_TEXTURE_TARGET_BUFFER ? 0 : 1; }
|
|
NVN_API(nvnTextureBuilderGetStorageClass) { ret_x(c, storage_class(&TB(c)->d)); }
|
|
NVN_API(nvnTextureBuilderGetFlags) { ret_x(c, (uint32_t)TB(c)->d.flags); }
|
|
// size_t nvnTextureBuilderGetStorageSize(const NVNtextureBuilder *b)
|
|
NVN_API(nvnTextureBuilderGetStorageSize) {
|
|
nvn_texlayout L;
|
|
nvn_tex_layout(&TB(c)->d, &L);
|
|
if (!L.bpb) rt_log("nvn: TextureBuilderGetStorageSize: unknown format %#x", TB(c)->d.format);
|
|
ret_x(c, L.size);
|
|
}
|
|
// size_t nvnTextureBuilderGetStorageAlignment(const NVNtextureBuilder *b)
|
|
NVN_API(nvnTextureBuilderGetStorageAlignment) {
|
|
nvn_texlayout L;
|
|
nvn_tex_layout(&TB(c)->d, &L);
|
|
ret_x(c, L.align);
|
|
}
|
|
|
|
// ---------- Textures
|
|
// The game re-initializes transient textures (G-buffer and the like) every frame at the same pool memory without
|
|
// registering the descriptor again, as an NVN descriptor refers to memory and stays valid on hardware. The table maps
|
|
// storage address -> a copy of the live texture so the back end can redirect a registration whose serial was finalized.
|
|
static nvn_hmap live = {.klen = 8};
|
|
static SRWLOCK live_lock = SRWLOCK_INIT;
|
|
|
|
static void live_put(const tex_t *o) {
|
|
if (!o->mem) return;
|
|
uint64_t k = (uintptr_t)o->mem;
|
|
tex_t *copy = malloc(sizeof *copy);
|
|
if (!copy) rt_fatal("nvn: out of memory for the live texture table");
|
|
*copy = *o;
|
|
AcquireSRWLockExclusive(&live_lock);
|
|
tex_t *old = nvn_hm_put(&live, &k, copy);
|
|
ReleaseSRWLockExclusive(&live_lock);
|
|
free(old);
|
|
}
|
|
|
|
static void live_del(const tex_t *o) {
|
|
if (!o->mem) return;
|
|
uint64_t k = (uintptr_t)o->mem;
|
|
AcquireSRWLockExclusive(&live_lock);
|
|
tex_t *cur = nvn_hm_get(&live, &k);
|
|
if (cur && cur->serial == o->serial) nvn_hm_del(&live, &k);
|
|
else cur = NULL;
|
|
ReleaseSRWLockExclusive(&live_lock);
|
|
free(cur);
|
|
}
|
|
|
|
int nvn_tex_live_at(const void *mem, tex_t *out) {
|
|
if (!mem) return 0;
|
|
uint64_t k = (uintptr_t)mem;
|
|
AcquireSRWLockShared(&live_lock);
|
|
const tex_t *cur = nvn_hm_get(&live, &k);
|
|
if (cur) *out = *cur;
|
|
ReleaseSRWLockShared(&live_lock);
|
|
return cur != NULL;
|
|
}
|
|
|
|
// Releases the back end's texture and the live entry of an initialized texture object.
|
|
static void tex_release(tex_t *o) {
|
|
live_del(o);
|
|
gfx_backend()->texture_release(o);
|
|
}
|
|
|
|
// NVNboolean nvnTextureInitialize(NVNtexture *t, const NVNtextureBuilder *b)
|
|
NVN_API(nvnTextureInitialize) {
|
|
tex_t *o = arg_p(c, 0);
|
|
const tex_builder_t *s = arg_p(c, 1);
|
|
if (o->magic == NVN_TEX_MAGIC && o->serial) { // initialized again without Finalize: release the old serial
|
|
NVN_LOG("texture %p initialized again without Finalize (serial %llu released)", (void *)o,
|
|
(unsigned long long)o->serial);
|
|
tex_release(o);
|
|
}
|
|
memset(o, 0, sizeof *o);
|
|
o->magic = NVN_TEX_MAGIC;
|
|
o->d = s->d;
|
|
o->pool = s->pool;
|
|
o->off = s->off;
|
|
o->has_layout = s->has_layout;
|
|
memcpy(o->layout, s->layout, 8);
|
|
nvn_tex_layout(&o->d, &o->L);
|
|
o->mem = s->pool && s->pool->mem ? s->pool->mem + s->off : NULL;
|
|
if (s->pkg_data) {
|
|
if (!o->mem || nvn_gpu_only((uintptr_t)o->mem))
|
|
rt_fatal("nvn: packaged texture data without CPU-visible storage");
|
|
memcpy(o->mem, s->pkg_data, (size_t)o->L.size);
|
|
nvn_guest_written((uintptr_t)o->mem, o->L.size);
|
|
}
|
|
o->serial = nvn_new_serial();
|
|
live_put(o);
|
|
NVN_LOG("Texture %p %ux%ux%u lv%u fmt=%#x tgt=%d flags=%#x size=%#llx", (void *)o, o->d.width, o->d.height,
|
|
o->d.depth, o->d.levels, o->d.format, o->d.target, o->d.flags, (unsigned long long)o->L.size);
|
|
ret_x(c, 1);
|
|
}
|
|
// void nvnTextureFinalize(NVNtexture *t)
|
|
NVN_API(nvnTextureFinalize) {
|
|
tex_t *o = arg_p(c, 0);
|
|
if (o->magic == NVN_TEX_MAGIC && o->serial) tex_release(o);
|
|
o->magic = 0;
|
|
}
|
|
#define CTX(c) ((const tex_t *)arg_p(c, 0))
|
|
NVN_API(nvnTextureGetStorageClass) { ret_x(c, storage_class(&CTX(c)->d)); }
|
|
NVN_API(nvnTextureGetFlags) { ret_x(c, (uint32_t)CTX(c)->d.flags); }
|
|
NVN_API(nvnTextureGetWidth) { ret_x(c, CTX(c)->d.width); }
|
|
NVN_API(nvnTextureGetHeight) { ret_x(c, CTX(c)->d.height); }
|
|
NVN_API(nvnTextureGetDepth) { ret_x(c, CTX(c)->d.depth); }
|
|
NVN_API(nvnTextureGetStorageSize) { ret_x(c, (uint32_t)(int)CTX(c)->L.size); }
|
|
// NVNboolean nvnTextureCompare(const NVNtexture *a, const NVNtexture *b)
|
|
NVN_API(nvnTextureCompare) {
|
|
const tex_t *x = arg_p(c, 0), *y = arg_p(c, 1);
|
|
ret_x(c, !memcmp(&x->d, &y->d, sizeof x->d) && x->mem == y->mem);
|
|
}
|
|
// int nvnTextureGetRawStorageClass(const NVNtexture *t): no prototype in nvn.h. Call sites compare the result with 1
|
|
// and set a status bit when it differs; the meaning is unverified, so 1.
|
|
NVN_API(nvnTextureGetRawStorageClass) { ret_x(c, 1); }
|
|
|
|
// ---------- Texture views
|
|
#define VW(c) ((view_t *)arg_p(c, 0))
|
|
NVN_API(nvnTextureViewSetDefaults) { memset(arg_p(c, 0), 0, sizeof(view_t)); }
|
|
NVN_API(nvnTextureViewSetLevels) {
|
|
uint32_t base = (uint32_t)arg_x(c, 1), n = (uint32_t)arg_x(c, 2);
|
|
if (base > 255 || n > 255) rt_fatal("nvn: texture view levels %d/%d", (int)base, (int)n);
|
|
view_t *v = VW(c);
|
|
v->set_levels = 1;
|
|
v->base_level = (uint8_t)base;
|
|
v->nlevels = (uint8_t)n;
|
|
}
|
|
NVN_API(nvnTextureViewSetLayers) {
|
|
uint32_t first = (uint32_t)arg_x(c, 1), n = (uint32_t)arg_x(c, 2);
|
|
if (first > 65535 || n > 65535) rt_fatal("nvn: texture view layers %d/%d", (int)first, (int)n);
|
|
view_t *v = VW(c);
|
|
v->set_layers = 1;
|
|
v->min_layer = (uint16_t)first;
|
|
v->nlayers = (uint16_t)n;
|
|
}
|
|
NVN_API(nvnTextureViewSetFormat) {
|
|
uint32_t f = (uint32_t)arg_x(c, 1);
|
|
if (f > 65535) rt_fatal("nvn: texture view format %d", (int)f);
|
|
view_t *v = VW(c);
|
|
v->set_format = 1;
|
|
v->format = (uint16_t)f;
|
|
}
|
|
NVN_API(nvnTextureViewSetTarget) {
|
|
uint32_t t = (uint32_t)arg_x(c, 1);
|
|
if (t > 255) rt_fatal("nvn: texture view target %d", (int)t);
|
|
view_t *v = VW(c);
|
|
v->set_target = 1;
|
|
v->target = (uint8_t)t;
|
|
}
|
|
// NVNboolean nvnTextureViewGetLayers(const NVNtextureView *v, int *min, int *n)
|
|
NVN_API(nvnTextureViewGetLayers) {
|
|
const view_t *v = arg_p(c, 0);
|
|
*(int *)arg_p(c, 1) = v->min_layer;
|
|
*(int *)arg_p(c, 2) = v->nlayers;
|
|
ret_x(c, v->set_layers);
|
|
}
|
|
|
|
uint64_t nvn_tex_view_offset(const tex_t *t, const view_t *v) {
|
|
if (!v) return 0;
|
|
uint64_t off = v->set_levels ? nvn_tex_level_offset(&t->d, v->base_level) : 0;
|
|
if (v->set_layers) off += (uint64_t)v->min_layer * t->L.layer_size;
|
|
return off;
|
|
}
|
|
// ptrdiff_t nvnTextureGetViewOffset(const NVNtexture *t, const NVNtextureView *v)
|
|
NVN_API(nvnTextureGetViewOffset) { ret_x(c, nvn_tex_view_offset(arg_p(c, 0), arg_p(c, 1))); }
|
|
|
|
// ---------- Texel access: the back end decodes the view's level and layer
|
|
// row / img: 0 means tightly packed
|
|
static void texel_strides(const tex_t *o, const NVNcopyRegion *r, uint64_t *row, uint64_t *img, const char *what) {
|
|
int bw, bh, bpb = nvn_fmt_bpb(o->d.format, &bw, &bh);
|
|
if (!bpb) rt_fatal("nvn: %s: unknown format %#x", what, o->d.format);
|
|
uint64_t wb = (uint64_t)(((uint32_t)r->width + bw - 1) / bw) * bpb, hb = ((uint32_t)r->height + bh - 1) / bh;
|
|
if (!*row) *row = wb;
|
|
if (!*img) *img = *row * hb;
|
|
if (*row < wb || *img < *row * hb) rt_fatal("nvn: %s: stride smaller than the region", what);
|
|
}
|
|
// void nvnTextureReadTexels[Strided](const NVNtexture *t, const NVNtextureView *v, const NVNcopyRegion *r, void *dst
|
|
// [, ptrdiff_t row, ptrdiff_t img])
|
|
static void read_texels(Cpu *c, int strided) {
|
|
const tex_t *o = arg_p(c, 0);
|
|
const NVNcopyRegion *r = arg_p(c, 2);
|
|
uint64_t row = strided ? arg_x(c, 4) : 0, img = strided ? arg_x(c, 5) : 0;
|
|
texel_strides(o, r, &row, &img, "ReadTexels");
|
|
NVN_BLOCKING();
|
|
gfx_backend()->read_texels(o, arg_p(c, 1), r, arg_p(c, 3), row, img);
|
|
}
|
|
// void nvnTextureWriteTexels[Strided](const NVNtexture *t, const NVNtextureView *v, const NVNcopyRegion *r, const void
|
|
// *src[, ptrdiff_t row, ptrdiff_t img])
|
|
static void write_texels(Cpu *c, int strided) {
|
|
const tex_t *o = arg_p(c, 0);
|
|
const view_t *v = arg_p(c, 1);
|
|
const NVNcopyRegion *r = arg_p(c, 2);
|
|
uint64_t row = strided ? arg_x(c, 4) : 0, img = strided ? arg_x(c, 5) : 0;
|
|
texel_strides(o, r, &row, &img, "WriteTexels");
|
|
if (v && v->set_format && v->format != o->d.format) rt_fatal("nvn: WriteTexels: format-reinterpreting view not supported");
|
|
NVN_BLOCKING();
|
|
gfx_backend()->write_texels(o, v, r, arg_p(c, 3), row, img);
|
|
}
|
|
NVN_API(nvnTextureReadTexels) { read_texels(c, 0); }
|
|
NVN_API(nvnTextureReadTexelsStrided) { read_texels(c, 1); }
|
|
NVN_API(nvnTextureWriteTexels) { write_texels(c, 0); }
|
|
NVN_API(nvnTextureWriteTexelsStrided) { write_texels(c, 1); }
|
|
// void nvnTexture{Flush,Invalidate}Texels(const NVNtexture *t, const NVNtextureView *v, const NVNcopyRegion *r): texel
|
|
// access is coherent
|
|
NVN_API(nvnTextureFlushTexels) { (void)c; }
|
|
NVN_API(nvnTextureInvalidateTexels) { (void)c; }
|
|
|
|
// ---------- Samplers
|
|
#define SB(c) (&((samp_builder_t *)arg_p(c, 0))->p)
|
|
// void nvnSamplerBuilderSetDefaults(NVNsamplerBuilder *b): keeps the device; GL defaults
|
|
NVN_API(nvnSamplerBuilderSetDefaults) {
|
|
samp_builder_t *b = arg_p(c, 0);
|
|
NVNdevice *dev = b->dev;
|
|
memset(b, 0, sizeof *b);
|
|
b->dev = dev;
|
|
samp_par *o = &b->p;
|
|
o->minf = NVN_MIN_FILTER_NEAREST_MIPMAP_LINEAR;
|
|
o->magf = NVN_MAG_FILTER_LINEAR;
|
|
o->ws = o->wt = o->wr = NVN_WRAP_MODE_REPEAT;
|
|
o->cmp_func = NVN_COMPARE_FUNC_LESS;
|
|
o->lod_min = -1000.f;
|
|
o->lod_max = 1000.f;
|
|
o->aniso = 1.f;
|
|
}
|
|
NVN_API(nvnSamplerBuilderSetDevice) { ((samp_builder_t *)arg_p(c, 0))->dev = arg_p(c, 1); }
|
|
NVN_API(nvnSamplerBuilderSetMinMagFilter) {
|
|
samp_par *p = SB(c);
|
|
p->minf = (uint8_t)arg_x(c, 1);
|
|
p->magf = (uint8_t)arg_x(c, 2);
|
|
}
|
|
NVN_API(nvnSamplerBuilderSetWrapMode) {
|
|
samp_par *p = SB(c);
|
|
p->ws = (uint8_t)arg_x(c, 1);
|
|
p->wt = (uint8_t)arg_x(c, 2);
|
|
p->wr = (uint8_t)arg_x(c, 3);
|
|
}
|
|
// void nvnSamplerBuilderSetLodClamp(NVNsamplerBuilder *b, float min, float max): the floats arrive in v0, v1
|
|
NVN_API(nvnSamplerBuilderSetLodClamp) {
|
|
samp_par *p = SB(c);
|
|
p->lod_min = arg_s(c, 0);
|
|
p->lod_max = arg_s(c, 1);
|
|
}
|
|
NVN_API(nvnSamplerBuilderSetLodBias) { SB(c)->lod_bias = arg_s(c, 0); }
|
|
NVN_API(nvnSamplerBuilderSetCompare) {
|
|
samp_par *p = SB(c);
|
|
p->cmp_mode = (uint8_t)arg_x(c, 1);
|
|
p->cmp_func = (uint8_t)arg_x(c, 2);
|
|
}
|
|
NVN_API(nvnSamplerBuilderSetBorderColor) { memcpy(SB(c)->border, arg_p(c, 1), 16); }
|
|
NVN_API(nvnSamplerBuilderSetMaxAnisotropy) { SB(c)->aniso = arg_s(c, 0); }
|
|
NVN_API(nvnSamplerBuilderSetLodSnap) { SB(c)->lod_snap = arg_s(c, 0); } // stored for queries; no host equivalent
|
|
// void nvnSamplerBuilderGetMinMagFilter(const NVNsamplerBuilder *b, NVNminFilter *min, NVNmagFilter *mag)
|
|
NVN_API(nvnSamplerBuilderGetMinMagFilter) {
|
|
const samp_par *p = SB(c);
|
|
*(uint32_t *)arg_p(c, 1) = p->minf;
|
|
*(uint32_t *)arg_p(c, 2) = p->magf;
|
|
}
|
|
// void nvnSamplerBuilderGetWrapMode(const NVNsamplerBuilder *b, NVNwrapMode *s, NVNwrapMode *t, NVNwrapMode *r)
|
|
NVN_API(nvnSamplerBuilderGetWrapMode) {
|
|
const samp_par *p = SB(c);
|
|
*(uint32_t *)arg_p(c, 1) = p->ws;
|
|
*(uint32_t *)arg_p(c, 2) = p->wt;
|
|
*(uint32_t *)arg_p(c, 3) = p->wr;
|
|
}
|
|
NVN_API(nvnSamplerBuilderSetReductionFilter) {
|
|
uint32_t r = (uint32_t)arg_x(c, 1);
|
|
if (r > 2) rt_fatal("nvn: invalid sampler reduction %u", r);
|
|
SB(c)->reduction = (uint8_t)r;
|
|
}
|
|
NVN_API(nvnSamplerBuilderGetReductionFilter) { ret_x(c, SB(c)->reduction); }
|
|
NVN_API(nvnSamplerGetReductionFilter) { ret_x(c, ((const samp_t *)arg_p(c, 0))->p.reduction); }
|
|
// NVNboolean nvnSamplerInitialize(NVNsampler *s, const NVNsamplerBuilder *b)
|
|
NVN_API(nvnSamplerInitialize) {
|
|
samp_t *o = arg_p(c, 0);
|
|
o->magic = NVN_SAMP_MAGIC;
|
|
o->p = ((const samp_builder_t *)arg_p(c, 1))->p;
|
|
ret_x(c, 1);
|
|
}
|
|
NVN_API(nvnSamplerFinalize) { ((samp_t *)arg_p(c, 0))->magic = 0; }
|
|
|
|
// ---------- Programs
|
|
// NVNboolean nvnProgramInitialize(NVNprogram *p, NVNdevice *d)
|
|
NVN_API(nvnProgramInitialize) {
|
|
prog_t *o = arg_p(c, 0);
|
|
memset(o, 0, sizeof *o);
|
|
o->magic = NVN_PROG_MAGIC;
|
|
ret_x(c, 1);
|
|
}
|
|
static void prog_release(prog_t *o) {
|
|
if (o->serial) gfx_backend()->program_release(o);
|
|
o->serial = 0;
|
|
}
|
|
// void nvnProgramFinalize(NVNprogram *p)
|
|
NVN_API(nvnProgramFinalize) {
|
|
prog_t *o = arg_p(c, 0);
|
|
prog_release(o);
|
|
o->magic = 0;
|
|
}
|
|
static int prog_set_shaders(prog_t *o, int n, const NVNshaderData *sd) {
|
|
if (n < 0 || n > 6 || (n && !sd)) {
|
|
rt_log("nvn: ProgramSetShaders with %d stages at %p", n, (const void *)sd);
|
|
return 0;
|
|
}
|
|
prog_release(o);
|
|
o->nstage = (uint32_t)n;
|
|
if (n) memcpy(o->sd, sd, (size_t)n * sizeof *sd);
|
|
o->serial = nvn_new_serial();
|
|
NVN_LOG("ProgramSetShaders %p n=%d data0=%#llx", (void *)o, n, n ? (unsigned long long)sd[0].data : 0ull);
|
|
return 1;
|
|
}
|
|
// NVNboolean nvnProgramSetShaders(NVNprogram *p, int n, const NVNshaderData *sd)
|
|
NVN_API(nvnProgramSetShaders) {
|
|
NVN_BLOCKING();
|
|
ret_x(c, prog_set_shaders(arg_p(c, 0), (int)arg_x(c, 1), arg_p(c, 2)));
|
|
}
|
|
// NVNboolean nvnProgramSetShadersExt(NVNprogram *p, int n, const NVNshaderDataExt *sd): no prototype in nvn.h; the
|
|
// entries are {data, control, reserved} (0x18 bytes, reserved observed 0)
|
|
typedef struct { NVNbufferAddress data; const void *control; uint64_t reserved; } shader_data_ext;
|
|
_Static_assert(sizeof(shader_data_ext) == 0x18, "NVNshaderDataExt stride");
|
|
NVN_API(nvnProgramSetShadersExt) {
|
|
NVN_BLOCKING();
|
|
prog_t *o = arg_p(c, 0);
|
|
int n = (int)arg_x(c, 1);
|
|
const shader_data_ext *sd = arg_p(c, 2);
|
|
if (n < 0 || n > 6 || (n && !sd)) {
|
|
rt_log("nvn: ProgramSetShadersExt with %d stages at %p", n, (const void *)sd);
|
|
ret_x(c, 0);
|
|
return;
|
|
}
|
|
NVNshaderData plain[6];
|
|
for (int i = 0; i < n; i++) {
|
|
if (sd[i].reserved)
|
|
rt_fatal("nvn: ProgramSetShadersExt: unknown non-zero field %#llx in stage %d", (unsigned long long)sd[i].reserved, i);
|
|
plain[i] = (NVNshaderData){sd[i].data, sd[i].control};
|
|
}
|
|
ret_x(c, prog_set_shaders(o, n, plain));
|
|
}
|
|
|
|
// ---------- State objects: packed in place (nvn_obj.h)
|
|
#define ST(T, c) ((T *)arg_p(c, 0))
|
|
NVN_API(nvnBlendStateSetDefaults) {
|
|
*ST(blend_st, c) = (blend_st){0, NVN_BLEND_FUNC_ONE, NVN_BLEND_FUNC_ZERO, NVN_BLEND_FUNC_ONE, NVN_BLEND_FUNC_ZERO,
|
|
NVN_BLEND_EQUATION_ADD, NVN_BLEND_EQUATION_ADD};
|
|
}
|
|
NVN_API(nvnBlendStateSetBlendTarget) { ST(blend_st, c)->target = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnBlendStateSetBlendFunc) {
|
|
blend_st *o = ST(blend_st, c);
|
|
o->src_rgb = (uint32_t)arg_x(c, 1);
|
|
o->dst_rgb = (uint32_t)arg_x(c, 2);
|
|
o->src_a = (uint32_t)arg_x(c, 3);
|
|
o->dst_a = (uint32_t)arg_x(c, 4);
|
|
}
|
|
NVN_API(nvnBlendStateSetBlendEquation) {
|
|
blend_st *o = ST(blend_st, c);
|
|
o->eq_rgb = (uint32_t)arg_x(c, 1);
|
|
o->eq_a = (uint32_t)arg_x(c, 2);
|
|
}
|
|
NVN_API(nvnColorStateSetDefaults) { *ST(color_st, c) = (color_st){0, NVN_LOGIC_OP_COPY, NVN_ALPHA_FUNC_ALWAYS}; }
|
|
// void nvnColorStateSetBlendEnable(NVNcolorState *s, int index, NVNboolean enable)
|
|
NVN_API(nvnColorStateSetBlendEnable) {
|
|
color_st *o = ST(color_st, c);
|
|
int i = (int)arg_x(c, 1);
|
|
if (i < 0 || i > 7) return;
|
|
o->blend_en = nvn_arg_bool(c, 2) ? (o->blend_en | 1u << i) : (o->blend_en & ~(1u << i));
|
|
}
|
|
NVN_API(nvnChannelMaskStateSetDefaults) { ST(chmask_st, c)->mask = 0xffffffffu; }
|
|
// void nvnChannelMaskStateSetChannelMask(NVNchannelMaskState *s, int index, NVNboolean r, g, b, a)
|
|
NVN_API(nvnChannelMaskStateSetChannelMask) {
|
|
int i = (int)arg_x(c, 1);
|
|
if (i < 0 || i > 7) return;
|
|
uint32_t m = (nvn_arg_bool(c, 2) ? 1u : 0) | (nvn_arg_bool(c, 3) ? 2u : 0) | (nvn_arg_bool(c, 4) ? 4u : 0) |
|
|
(nvn_arg_bool(c, 5) ? 8u : 0);
|
|
chmask_st *o = ST(chmask_st, c);
|
|
o->mask = (o->mask & ~(0xfu << (4 * i))) | m << (4 * i);
|
|
}
|
|
NVN_API(nvnMultisampleStateSetDefaults) {
|
|
memset(arg_p(c, 0), 0, sizeof(ms_st));
|
|
ST(ms_st, c)->enable = 1;
|
|
}
|
|
NVN_API(nvnMultisampleStateSetMultisampleEnable) { ST(ms_st, c)->enable = nvn_arg_bool(c, 1) != 0; }
|
|
NVN_API(nvnMultisampleStateSetSamples) { ST(ms_st, c)->samples = (uint8_t)arg_x(c, 1); }
|
|
NVN_API(nvnMultisampleStateSetAlphaToCoverageEnable) { ST(ms_st, c)->a2c = nvn_arg_bool(c, 1) != 0; }
|
|
NVN_API(nvnMultisampleStateSetSampleLocationsEnable) { ST(ms_st, c)->sl_en = nvn_arg_bool(c, 1) != 0; }
|
|
// void nvnMultisampleStateSetSampleLocations(NVNmultisampleState *s, int start, int n, const float *v): programmable
|
|
// sample positions are not replayed
|
|
NVN_API(nvnMultisampleStateSetSampleLocations) { (void)c; }
|
|
NVN_API(nvnPolygonStateSetDefaults) {
|
|
*ST(poly_st, c) = (poly_st){NVN_FACE_NONE, NVN_FRONT_FACE_CCW, NVN_POLYGON_MODE_FILL, 0};
|
|
}
|
|
NVN_API(nvnPolygonStateSetCullFace) { ST(poly_st, c)->cull = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnPolygonStateSetFrontFace) { ST(poly_st, c)->front = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnPolygonStateSetPolygonMode) { ST(poly_st, c)->mode = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnPolygonStateSetPolygonOffsetEnables) { ST(poly_st, c)->offset_en = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnDepthStencilStateSetDefaults) {
|
|
*ST(ds_st, c) = (ds_st){0, 1, NVN_DEPTH_FUNC_LESS, 0, NVN_STENCIL_FUNC_ALWAYS, NVN_STENCIL_FUNC_ALWAYS,
|
|
NVN_STENCIL_OP_KEEP, NVN_STENCIL_OP_KEEP, NVN_STENCIL_OP_KEEP,
|
|
NVN_STENCIL_OP_KEEP, NVN_STENCIL_OP_KEEP, NVN_STENCIL_OP_KEEP};
|
|
}
|
|
NVN_API(nvnDepthStencilStateSetDepthTestEnable) { ST(ds_st, c)->dt = nvn_arg_bool(c, 1) != 0; }
|
|
NVN_API(nvnDepthStencilStateSetDepthWriteEnable) { ST(ds_st, c)->dw = nvn_arg_bool(c, 1) != 0; }
|
|
NVN_API(nvnDepthStencilStateSetDepthFunc) { ST(ds_st, c)->df = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnDepthStencilStateSetStencilTestEnable) { ST(ds_st, c)->st = nvn_arg_bool(c, 1) != 0; }
|
|
// void nvnDepthStencilStateSetStencilFunc(NVNdepthStencilState *s, NVNface face, NVNstencilFunc func)
|
|
NVN_API(nvnDepthStencilStateSetStencilFunc) {
|
|
ds_st *o = ST(ds_st, c);
|
|
uint32_t face = (uint32_t)arg_x(c, 1), fn = (uint32_t)arg_x(c, 2);
|
|
if (face & NVN_FACE_FRONT) o->sf_f = fn;
|
|
if (face & NVN_FACE_BACK) o->sf_b = fn;
|
|
}
|
|
// void nvnDepthStencilStateSetStencilOp(NVNdepthStencilState *s, NVNface face, NVNstencilOp fail, zfail, zpass)
|
|
NVN_API(nvnDepthStencilStateSetStencilOp) {
|
|
ds_st *o = ST(ds_st, c);
|
|
uint32_t face = (uint32_t)arg_x(c, 1), fail = (uint32_t)arg_x(c, 2), zfail = (uint32_t)arg_x(c, 3),
|
|
zpass = (uint32_t)arg_x(c, 4);
|
|
if (face & NVN_FACE_FRONT) { o->fail_f = fail; o->zfail_f = zfail; o->zpass_f = zpass; }
|
|
if (face & NVN_FACE_BACK) { o->fail_b = fail; o->zfail_b = zfail; o->zpass_b = zpass; }
|
|
}
|
|
NVN_API(nvnVertexAttribStateSetDefaults) { *ST(va_st, c) = (va_st){0, 0, 0}; }
|
|
// void nvnVertexAttribStateSetFormat(NVNvertexAttribState *s, NVNformat format, ptrdiff_t offset)
|
|
NVN_API(nvnVertexAttribStateSetFormat) {
|
|
int64_t off = (int64_t)arg_x(c, 2);
|
|
if (off < 0 || off >= (1 << 19)) rt_fatal("nvn: vertex attribute offset %lld outside the packed range", (long long)off);
|
|
va_st *o = ST(va_st, c);
|
|
o->fmt = (uint32_t)arg_x(c, 1);
|
|
o->offset = (uint32_t)off;
|
|
}
|
|
NVN_API(nvnVertexAttribStateSetStreamIndex) { ST(va_st, c)->stream = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnVertexStreamStateSetDefaults) { *ST(vs_st, c) = (vs_st){0, 0}; }
|
|
NVN_API(nvnVertexStreamStateSetStride) { ST(vs_st, c)->stride = (uint32_t)arg_x(c, 1); }
|
|
NVN_API(nvnVertexStreamStateSetDivisor) { ST(vs_st, c)->divisor = (uint32_t)arg_x(c, 1); }
|
|
|
|
// ---------- Windows
|
|
#define WB(c) ((win_builder_t *)arg_p(c, 0))
|
|
#define WN(c) ((win_t *)arg_p(c, 0))
|
|
NVN_API(nvnWindowBuilderSetDefaults) {
|
|
win_builder_t *b = WB(c);
|
|
NVNdevice *dev = b->dev;
|
|
memset(b, 0, sizeof *b);
|
|
b->dev = dev;
|
|
b->interval = 1;
|
|
}
|
|
NVN_API(nvnWindowBuilderSetDevice) { WB(c)->dev = arg_p(c, 1); }
|
|
NVN_API(nvnWindowBuilderSetNativeWindow) { WB(c)->native = arg_p(c, 1); }
|
|
// void nvnWindowBuilderSetTextures(NVNwindowBuilder *b, int n, NVNtexture *const *textures)
|
|
NVN_API(nvnWindowBuilderSetTextures) {
|
|
win_builder_t *b = WB(c);
|
|
int n = (int)arg_x(c, 1);
|
|
NVNtexture *const *t = arg_p(c, 2);
|
|
if (n < 0 || n > 4 || (n && !t)) rt_fatal("nvn: WindowBuilderSetTextures with %d textures at %p", n, (const void *)t);
|
|
b->ntex = n;
|
|
for (int i = 0; i < n; i++) b->tex[i] = t[i];
|
|
}
|
|
// TOTK sets two active images; the limit bounds what WindowAcquireTexture hands out. No prototype in nvn.h.
|
|
NVN_API(nvnWindowBuilderSetNumActiveTextures) {
|
|
int n = (int)arg_x(c, 1);
|
|
if (n < 2 || n > 4) rt_fatal("nvn: unsupported active texture count %d", n);
|
|
WB(c)->active = n;
|
|
}
|
|
NVN_API(nvnWindowBuilderGetNumActiveTextures) {
|
|
const win_builder_t *b = WB(c);
|
|
ret_x(c, (uint32_t)(b->active ? b->active : b->ntex));
|
|
}
|
|
NVN_API(nvnWindowSetNumActiveTextures) {
|
|
win_t *w = WN(c);
|
|
int n = (int)arg_x(c, 1);
|
|
if (n < 2 || n > w->ntex) rt_fatal("nvn: invalid active window texture count %d", n);
|
|
w->active = n;
|
|
}
|
|
NVN_API(nvnWindowGetNumActiveTextures) {
|
|
const win_t *w = WN(c);
|
|
ret_x(c, (uint32_t)(w->active ? w->active : w->ntex));
|
|
}
|
|
NVN_API(nvnWindowBuilderSetPresentInterval) { WB(c)->interval = (int)arg_x(c, 1); }
|
|
// NVNboolean nvnWindowInitialize(NVNwindow *w, const NVNwindowBuilder *b)
|
|
NVN_API(nvnWindowInitialize) {
|
|
win_t *o = arg_p(c, 0);
|
|
const win_builder_t *s = arg_p(c, 1);
|
|
memset(o, 0, sizeof *o);
|
|
o->magic = NVN_WIN_MAGIC;
|
|
o->native = s->native;
|
|
o->ntex = s->ntex;
|
|
o->interval = s->interval;
|
|
o->active = s->active ? s->active : s->ntex;
|
|
if (o->ntex < 2 || o->ntex > 4 || o->active < 2 || o->active > o->ntex) {
|
|
rt_log("nvn: WindowInitialize with %d textures, %d active", o->ntex, o->active);
|
|
ret_x(c, 0);
|
|
return;
|
|
}
|
|
memcpy(o->tex, s->tex, sizeof o->tex);
|
|
const tex_t *t0 = (const tex_t *)o->tex[0];
|
|
NVN_LOG("WindowInitialize %d textures, interval %d, first %ux%u format %#x", o->ntex, o->interval,
|
|
t0 ? t0->d.width : 0, t0 ? t0->d.height : 0, t0 ? t0->d.format : 0);
|
|
ret_x(c, 1);
|
|
}
|
|
// void nvnWindowFinalize(NVNwindow *w): nothing to release (MNG lists it in g_nops)
|
|
NVN_API(nvnWindowFinalize) { (void)c; }
|
|
NVN_API(nvnWindowSetPresentInterval) {
|
|
win_t *o = WN(c);
|
|
int v = (int)arg_x(c, 1);
|
|
if (o->interval != v) NVN_LOG("present interval %d -> %d", o->interval, v);
|
|
o->interval = v;
|
|
}
|
|
// void nvnWindowSetCrop(NVNwindow *w, int x, int y, int w, int h)
|
|
NVN_API(nvnWindowSetCrop) {
|
|
win_t *o = WN(c);
|
|
int v[4] = {(int)arg_x(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3), (int)arg_x(c, 4)};
|
|
if (memcmp(o->crop, v, sizeof v)) NVN_LOG("WindowCrop %d,%d %dx%d", v[0], v[1], v[2], v[3]);
|
|
memcpy(o->crop, v, sizeof v);
|
|
}
|
|
|
|
// ---------- Present pacing
|
|
// The vsync thread ticks at a fixed 60 Hz whatever the monitor does, so one game frame lasts `interval` ticks. With
|
|
// the real-time frame step (frame_rate.c) game speed does not depend on the interval, so a frame is shown when it is
|
|
// ready, at most at the frame limit (TOTK_FPS_LIMIT: 30 to 360 per second or the monitor's rate): holding it to the
|
|
// 2nd or 3rd tick would only add latency and 50 ms frames.
|
|
#define VSYNC_HZ 60ull
|
|
static uint64_t present_period(const win_t *o) {
|
|
if (frame_step_realtime()) return frame_step_cap_ns();
|
|
return (o->interval > 0 ? (uint64_t)o->interval : 1ull) * 1000000000ull / VSYNC_HZ;
|
|
}
|
|
|
|
// MacNativeGames nvn_pace.h: the present slot is the previous slot plus one period. Early: wait for the slot. Late by
|
|
// less than a period: present at once and keep the phase. Behind by a period or more: restart from now, no catch-up
|
|
// burst. Returns the ns to wait.
|
|
static uint64_t pace(uint64_t *target, uint64_t now, uint64_t period) {
|
|
if (!period || !*target) { *target = now; return 0; }
|
|
uint64_t next = *target + period;
|
|
if (now <= next) { *target = next; return next - now; }
|
|
*target = now - next < period ? next : now;
|
|
return 0;
|
|
}
|
|
|
|
static void sleep_until(uint64_t deadline) {
|
|
for (;;) {
|
|
uint64_t now = kern_now_ns();
|
|
if (now >= deadline) return;
|
|
uint64_t d = deadline - now;
|
|
if (d >= 2000000) Sleep((DWORD)(d / 1000000 - 1));
|
|
else SwitchToThread();
|
|
}
|
|
}
|
|
|
|
// NVNwindowAcquireTextureResult nvnWindowAcquireTexture(NVNwindow *w, NVNsync *s, int *index): blocks here, not in the
|
|
// present, until the image's previous present plus one period has passed (the game often presents with a lock held).
|
|
// That is the time the next frame goes to the display, so the image is free as soon as that time is reached.
|
|
NVN_API(nvnWindowAcquireTexture) {
|
|
win_t *o = arg_p(c, 0);
|
|
sync_t *s = arg_p(c, 1);
|
|
int *out = arg_p(c, 2);
|
|
NVN_BLOCKING();
|
|
int idx = o->active ? o->next++ % o->active : 0;
|
|
*out = idx;
|
|
uint64_t at = idx < 4 ? o->avail[idx] : 0;
|
|
if (at) {
|
|
uint64_t p0 = perf_begin();
|
|
sleep_until(at);
|
|
perf_end(PERF_PACE, p0);
|
|
}
|
|
// The sync means "the display no longer reads this image". Presents are copies queued in submit order, so it is
|
|
// already satisfied: fenced = 3 with fence 0.
|
|
if (s) {
|
|
s->fenced = 3;
|
|
s->fence = 0;
|
|
}
|
|
ret_x(c, NVN_WINDOW_ACQUIRE_TEXTURE_RESULT_SUCCESS);
|
|
}
|
|
|
|
// void nvnQueuePresentTexture(NVNqueue *q, NVNwindow *w, int index): queues the copy and returns at once. The display
|
|
// time comes from the pacer: the back end's present thread holds the host present until then (back-edge pacing, as the
|
|
// Switch's display flips at vblank), so the present cadence does not follow the replay time; one period later the
|
|
// image is free for the next acquire. Added latency: the slack of the frame (period minus its work), as on the Switch.
|
|
NVN_API(nvnQueuePresentTexture) {
|
|
win_t *o = arg_p(c, 1);
|
|
int idx = (int)arg_x(c, 2);
|
|
movie_tick(c); // ov_movie.c: hand over decoded movie frames that are due; needs the core, so before the wait
|
|
NVN_BLOCKING();
|
|
if (o->magic != NVN_WIN_MAGIC || idx < 0 || idx >= o->ntex || idx >= 4 || !o->tex[idx])
|
|
rt_fatal("nvn: QueuePresentTexture of image %d on window %p", idx, (void *)o);
|
|
o->presents++;
|
|
const tex_t *t = (const tex_t *)o->tex[idx];
|
|
uint64_t period = present_period(o), now = kern_now_ns();
|
|
uint64_t slot = now + pace(&o->pace, now, period);
|
|
gfx_backend()->present(t, o->crop, slot);
|
|
o->ticket[idx] = o->presents;
|
|
o->avail[idx] = slot + period;
|
|
frame_step_present(slot, o->interval); // frame_rate.c: the real-time step follows the display times
|
|
rt_frame_presented(t, o->presents);
|
|
}
|