Files
NX2navi/runtime/ov_movie2.c
chrg6 d8c046acc4 Destroy a movie decoder's system events and free their table entries when the decoder goes.
The review of the system-event change (Ev-1) found the table still filling. Entries in host memory now count as live
for good in the full-table scan, but their objects are freed: ov_movie2.c ov_dtor freed the Video with in_ev and out_ev
still bound, so each decoder left two dead entries that nothing reclaimed (AUDIT F-1-08 measured about two entries per
reload of a save, which E-4's verifier tied to the decoder pair), and their four kernel handles stayed open. A long
session would end with "kern_bind_system_event: table full" once no dead guest entry was left to take.

kernel.c gains kern_unbind_system_event(c, ev). On a guest thread it calls the SDK's nn::os::DestroySystemEvent, which
zeroes the state byte and closes both handles, and then removes the entry under g_se_lock (the last entry takes its
place). The destroy comes first, so a signal still in flight finds the event destroyed or its handle no longer naming it
and skips it, rather than finding no entry and binding again on a host thread. Off a guest thread, or when the SDK lacks
the function (logged once), only the entry goes. ov_dtor calls it for each bound event after finalize has joined the
decoder and convert threads, then frees the Video. net_offline.c frees its HostTask with a bound event in the same way;
that file is outside this change and is handed to the coordinator.

Not reproduced before the change in a game run. tests/system_event_check.c fills the table with live guest events and
the movie's two host-memory events, unbinds one: it leaves the table, its handles are closed and its state byte is 0,
the other keeps its entry, and a second unbind changes nothing; with the table full again and the last host entry
unbound, a new guest bind succeeds where it would have ended with "table full". The check passes in both modes,
movie_planes_check passes, and the compile check of every runtime file (host.h changed) passes, adding no warnings in
kernel.c or ov_movie2.c with -Wall -Wextra.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:46 -05:00

959 lines
45 KiB
C

// Switch 2 movie playback: movie::VideoDecoder (subsdk0) answered by the host with dav1d.
//
// The title's movies are WebM with AV1 video only (Event/Movie/*.webm, 1920x1080 60 fps, 8-bit 4:2:0, limited
// range BT.709); their sound is a separate stream the game plays alongside (Sound/Resource/Stream/<movie>.bwav),
// so a movie that runs for its real length is also what makes the title sequence audible. The SDK's
// nn::movie::MediaExtractor demuxes as it is; subsdk0's VideoDecoder sits on nn::codec::video (a video server this
// host does not have), so the public VideoDecoder methods the game imports are replaced here. VideoFrame stays
// subsdk0's own object; its layout (subsdk0 VideoFrame::SetBuffer / SetPixelPlaneData / SetTimeStamp /
// SetEndOfStreamState, sizeof 0x90):
// +0x08 + 8 i plane i data (plane 0 = the buffer of SetBuffer) +0x28 + 8 i plane i filled size
// +0x48 buffer capacity +0x78 time stamp (TimeSpan, ns)
// +0x80 pixel format +0x84 pixel storage type +0x88 end of stream (byte)
// nn::movie::Buffer (sdk): +0x00 data, +0x08 capacity, +0x10 filled size, +0x18 time stamp, +0x20 flags (8 = end
// of stream, as the game sets it at main+0x3b60014).
//
// Decoding is asynchronous, as on the hardware: DecodeAsync queues a copy of the sample and returns; one host thread
// per decoder runs dav1d and converts each picture to NV12 (the game's frame format: it sizes each frame buffer
// width * height * 3 / 2 and reads two planes) and to RGBA for the window. Decoding on the caller's thread stalled
// the game's movie loop (it also acquires frames) by up to 90 ms on heavy frames: visible stutter.
// Back-pressure: while QREADY samples or pictures wait, DecodeAsync answers R_BUSY and the input-ready event is
// clear; the game's decode step (main+0x3b5e600) treats any non-zero result as "try again" on that event.
// AcquireVideoFrame answers 0x7b when nothing is ready (subsdk0 VideoDecoder::AcquireVideoFrame's own result).
// The game draws the frame with its own shaders (a YUV compute pass). Each frame is debanded first (flat areas
// averaged with sub-level precision), so the encoder's flattened gradients (skies, clouds) do not show as hard bands,
// and written back to NV12 with a 4x4 ordered dither. With NX2NAVI_MOVIE_OVERLAY=1 the host instead shows the frame
// over the game image (gfx_present.c gfx_movie_frame_own), converted here: BT.709 limited range, bilinear chroma,
// the same dither. NX2NAVI_MOVIE_DEBAND=0 turns the deband off.
// NX2NAVI_MOVIES=off skips movies instead (romfs.c movie_skipped); this title then quits, so for testing use
// NX2NAVI_MOVIE_SKIP=<seconds>: a movie ends as if it had played to its end once its samples reach that time (the next
// sample becomes the end-of-stream marker, later ones are dropped), and the game goes on as after a whole movie.
#include <dav1d/dav1d.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "nvn_api.h"
#include "platform/platform.h"
#define R_NO_FRAME 0x7bu
#define R_BUSY 0x7du // DecodeAsync while the queues are full: the game retries on its input-ready event
#define BUFFER_FLAG_EOS 0x8u
#define QMAX 8 // decoded pictures held at most
#define INMAX 8 // samples waiting for the decoder thread at most
#define QREADY 4 // input-ready while fewer than this many samples and pictures wait
#define NFRAMES 16 // registered VideoFrames
#define CQMAX 2 // decoded pictures waiting for the convert thread at most
typedef struct { uint8_t *nv12, *rgba; int w, h; int64_t ts; int eos; } Picture;
typedef struct { uint8_t *data; size_t size; int64_t ts; int eos; } Sample;
typedef struct { Dav1dPicture pic; uint64_t gen; int64_t ts; int eos; } Decoded; // for the convert thread
typedef struct {
PlatMutex lock; // queues, frames, flags
PlatMutex codec; // dav1d: the decoder thread while it decodes, Flush / Finalize
PlatCond work; // a sample arrived, or quit
PlatThread *thread;
int run_index; // which movie of the run this is, counted from 1 as decoders come up (for the log)
// The convert thread debands and converts what dav1d decoded while the decoder thread decodes the next picture:
// one after the other on the decoder thread the two took longer than the 16.7 ms a 60 fps movie frame has
PlatThread *convert;
PlatCond conv; // a decoded picture arrived, a slot freed, or quit
Decoded cq[CQMAX];
int cqh, cqn;
// The convert thread's deband planes (16-bit Y, U and V, plane_room samples of luma), kept from picture to picture
// and made again only for a larger one: made per picture they were 6.2 MB of fresh pages each at 1080p, 60 times a
// second. finalize frees them once the convert thread is joined.
uint16_t *y16, *u16, *v16;
size_t plane_room;
Dav1dContext *dav1d;
uint8_t in_ev[0x40], out_ev[0x40]; // nn::os::SystemEventType each
int in_bound, out_bound;
uint64_t frame[NFRAMES];
int busy[NFRAMES], nframe;
Picture q[QMAX];
int qh, qn;
Sample in[INMAX];
int ih, in_n;
int quit, eos_queued;
uint64_t generation; // bumped by Flush: pictures of an older one are dropped
uint64_t decoded, shown;
int live; // counted in movies_live: from Initialize, or a sample after a Flush, to the end-of-stream
// picture, Finalize or the destructor
// The first shown picture's time stamp and when it was shown, for the pacing report's distance between the
// picture and real time; clock_on: they hold (cleared by Flush and Initialize)
int64_t ts0;
uint64_t shown0_ns;
int clock_on;
// The game fetches pictures ahead into a list of its own (main+0x3b61df0) and lets go of each when the next one
// takes its place on screen (ReleaseVideoFrame), so the distance to real time is measured there too: the newest
// released picture's, and how far it moved since the first released one (the drift), in ns
int64_t rel_lag, rel_lag0;
int rel_on;
} Video;
static Video *video_of(void *obj) { return obj ? *(Video **)obj : NULL; }
static int movies_live; // decoders playing (Video.live); frame_rate.c movie_cap_hz holds the frame rate meanwhile
int ov_movie_live(void) { return __atomic_load_n(&movies_live, __ATOMIC_RELAXED) > 0; }
static void set_live(Video *v, int live) { // the guest's decode and acquire threads may race here
if (__atomic_exchange_n(&v->live, live, __ATOMIC_RELAXED) != live)
__atomic_add_fetch(&movies_live, live ? 1 : -1, __ATOMIC_RELAXED);
}
// Time summed for the pacing report (one movie plays at a time): on the decoder thread waiting for the game's next
// sample, dav1d_send_data and dav1d_get_picture; on the convert thread make_picture (deband and conversion)
static uint64_t t_wait_ns, t_send_ns, t_get_ns, t_make_ns, n_made; // n_made: pictures converted
// under v->lock
static void update_events(Video *v) {
int ready = v->qn < QREADY && v->in_n < QREADY && !v->eos_queued;
if (v->in_bound) {
if (ready) kern_signal_system_event((uint64_t)(uintptr_t)v->in_ev);
else kern_clear_system_event((uint64_t)(uintptr_t)v->in_ev);
}
if (v->out_bound) {
if (v->qn) kern_signal_system_event((uint64_t)(uintptr_t)v->out_ev);
else kern_clear_system_event((uint64_t)(uintptr_t)v->out_ev);
}
}
static void picture_free(Picture *p) {
free(p->nv12);
free(p->rgba);
p->nv12 = p->rgba = NULL;
}
// under v->lock
static void clear_queues(Video *v) {
while (v->qn) {
picture_free(&v->q[v->qh]);
v->qh = (v->qh + 1) % QMAX;
v->qn--;
}
while (v->in_n) {
free(v->in[v->ih].data);
v->ih = (v->ih + 1) % INMAX;
v->in_n--;
}
v->eos_queued = 0;
}
// Deband (the approach of mpv's deband filter, our own code): the movies' flat gradients (skies, clouds) are encoded
// as blocks of one level with hard contours between them. For each sample, four neighbours at a pseudo-random offset
// of 4..16 samples (rotated per pixel, so no pattern repeats) are compared with it; when all lie within THRESH levels
// the area is flat and the sample becomes their mean with 4 bits of fraction, else it keeps its value. Edges and
// texture differ by more than that and stay as they are. Output: plane * 16.
#define DEBAND_THRESH 3
// The deband taps: radii 4..16 at golden-angle rotations, so every tap of a sample at least DEBAND_REACH away from
// each edge lies inside the plane.
#define DEBAND_REACH 16
static int16_t deband_off[64][2];
static volatile int32_t deband_ready;
static void deband_table(void) {
if (__atomic_load_n(&deband_ready, __ATOMIC_ACQUIRE)) return;
for (int i = 0; i < 64; i++) {
double r = 4 + (i * 12) / 63.0, t = i * 2.39996323;
deband_off[i][0] = (int16_t)(r * __builtin_cos(t)), deband_off[i][1] = (int16_t)(r * __builtin_sin(t));
}
__atomic_store_n(&deband_ready, 1, __ATOMIC_RELEASE);
}
// One sample near an edge: a tap outside the plane keeps the sample as it is
static inline uint16_t deband_edge(const uint8_t *src, int w, int h, int stride, int step, int x, int y) {
int c = src[(size_t)y * stride + x * step];
const int16_t *d = deband_off[(x * 7 + y * 13 + ((x ^ y) >> 2)) & 63];
int dx = d[0], dy = d[1];
int xa = x + dx, xb = x - dx, ya = y + dy, yb = y - dy;
int xc = x - dy, xd = x + dy, yc = y + dx, yd = y - dx;
if (xa < 0 || xb < 0 || xc < 0 || xd < 0 || xa >= w || xb >= w || xc >= w || xd >= w || ya < 0 || yb < 0 ||
yc < 0 || yd < 0 || ya >= h || yb >= h || yc >= h || yd >= h)
return (uint16_t)(c * 16);
int s0 = src[(size_t)ya * stride + xa * step], s1 = src[(size_t)yb * stride + xb * step];
int s2 = src[(size_t)yc * stride + xc * step], s3 = src[(size_t)yd * stride + xd * step];
int m0 = abs(s0 - c), m1 = abs(s1 - c), m2 = abs(s2 - c), m3 = abs(s3 - c);
if (m0 > DEBAND_THRESH || m1 > DEBAND_THRESH || m2 > DEBAND_THRESH || m3 > DEBAND_THRESH) return (uint16_t)(c * 16);
return (uint16_t)((s0 + s1 + s2 + s3) * 4); // the mean, * 16
}
// Each sample becomes the mean of four taps around it (a rotated pair of mirrored offsets) when all four lie within
// DEBAND_THRESH of it, else stays as it is; the result is * 16, keeping the mean's fraction. Samples away from the
// edges, nearly all of them, skip the bounds tests and choose without a branch: 28 ms on one thread for a 1080p frame
// became 16 ms, and in play the stripes no longer overran the 16.7 ms a 60 fps movie frame has.
static void deband(const uint8_t *src, uint16_t *dst, int w, int h, int stride, int step, int y0, int y1) {
deband_table();
const int R = DEBAND_REACH;
ptrdiff_t ta[64], tb[64]; // each offset as a distance in the plane: (+dx, +dy) and (-dy, +dx); their mirrors negate
for (int i = 0; i < 64; i++) {
ta[i] = (ptrdiff_t)deband_off[i][1] * stride + (ptrdiff_t)deband_off[i][0] * step;
tb[i] = (ptrdiff_t)deband_off[i][0] * stride - (ptrdiff_t)deband_off[i][1] * step;
}
for (int y = y0; y < y1; y++) {
uint16_t *o = dst + (size_t)y * w;
if (y < R || y >= h - R || w <= 2 * R) {
for (int x = 0; x < w; x++) o[x] = deband_edge(src, w, h, stride, step, x, y);
continue;
}
for (int x = 0; x < R; x++) o[x] = deband_edge(src, w, h, stride, step, x, y);
const uint8_t *row = src + (size_t)y * stride;
const int y13 = y * 13;
for (int x = R; x < w - R; x++) {
const uint8_t *p = row + x * step;
int c = *p, k = (x * 7 + y13 + ((x ^ y) >> 2)) & 63;
const ptrdiff_t a = ta[k], b = tb[k];
int s0 = p[a], s1 = p[-a], s2 = p[b], s3 = p[-b];
int edge = (abs(s0 - c) > DEBAND_THRESH) | (abs(s1 - c) > DEBAND_THRESH) | (abs(s2 - c) > DEBAND_THRESH) |
(abs(s3 - c) > DEBAND_THRESH);
o[x] = (uint16_t)(edge ? c * 16 : (s0 + s1 + s2 + s3) * 4);
}
for (int x = w - R; x < w; x++) o[x] = deband_edge(src, w, h, stride, step, x, y);
}
}
// Debanded planes (* 16) -> RGBA, BT.709 limited range. Chroma is interpolated bilinearly between the 4:2:0 samples
// (centred between luma rows, co-sited with even columns); a 4x4 Bayer threshold is added before the final rounding
// to 8 bits, which turns the remaining sub-level steps into fine noise instead of bands.
static void to_rgba(const uint16_t *Y, const uint16_t *U, const uint16_t *V, uint8_t *out, int w, int h, int y0, int y1) {
static const int bayer[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}};
const int cw = w / 2, ch = h / 2;
for (int y = y0; y < y1; y++) {
// the two chroma rows around this luma row, weighted 3:1 toward the nearer one
int cy0 = (y - 1) >> 1, cy1 = cy0 + 1, wy1 = (y & 1) ? 1 : 3, wy0 = 4 - wy1;
if (cy0 < 0) cy0 = 0;
if (cy1 >= ch) cy1 = ch - 1;
const uint16_t *u0 = U + (size_t)cy0 * cw, *u1 = U + (size_t)cy1 * cw;
const uint16_t *v0 = V + (size_t)cy0 * cw, *v1 = V + (size_t)cy1 * cw, *yr = Y + (size_t)y * w;
uint8_t *o = out + (size_t)y * w * 4;
for (int x = 0; x < w; x++) {
int cx0 = x >> 1, cx1 = (x & 1) && cx0 + 1 < cw ? cx0 + 1 : cx0; // even: on the sample, odd: halfway
int wx1 = cx1 != cx0 ? 2 : 0, wx0 = 4 - wx1;
// chroma * 16 * 16 (plane * 16, weights summing to 16) -> >> 4: * 16
int u = ((u0[cx0] * wx0 + u0[cx1] * wx1) * wy0 + (u1[cx0] * wx0 + u1[cx1] * wx1) * wy1) >> 4;
int vv = ((v0[cx0] * wx0 + v0[cx1] * wx1) * wy0 + (v1[cx0] * wx0 + v1[cx1] * wx1) * wy1) >> 4;
int d = u - 128 * 16, e = vv - 128 * 16; // * 16
int cc = (yr[x] - 16 * 16) * 298; // * 16 * 256
int dith = bayer[y & 3][x & 3] * 256 + 128; // < 1 output step in the * 4096 unit
int r = (cc + 459 * e + dith) >> 12;
int g = (cc - 55 * d - 136 * e + dith) >> 12;
int b = (cc + 541 * d + dith) >> 12;
o[4 * x] = (uint8_t)(r < 0 ? 0 : r > 255 ? 255 : r);
o[4 * x + 1] = (uint8_t)(g < 0 ? 0 : g > 255 ? 255 : g);
o[4 * x + 2] = (uint8_t)(b < 0 ? 0 : b > 255 ? 255 : b);
o[4 * x + 3] = 255;
}
}
}
// The overlay conversion runs in NSTRIPES horizontal stripes on short-lived threads, in two phases (deband every
// plane, then convert), since the convert reads chroma rows of the neighbouring stripe. One thread took 27 ms per
// 1080p frame: too slow for 60 fps.
#define NSTRIPES 6
typedef struct {
int phase, y0, y1, w, h;
uint8_t *nv12;
uint16_t *Y16, *U16, *V16;
uint8_t *rgba;
const uint8_t *py, *pu, *pv; // phase 2: the decoder's 8-bit planes and their strides (luma, chroma)
ptrdiff_t ys, cs;
} Stripe;
// The debanded planes back into the NV12 frame (the game converts it itself), with a 4x4 ordered dither: the
// 4 fraction bits become texel-sized noise instead of bands.
static void to_nv12(const Stripe *t) {
static const int bayer[4][4] = {{0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}};
int w = t->w;
for (int y = t->y0; y < t->y1; y++)
for (int x = 0; x < w; x++) {
int v = (t->Y16[(size_t)y * w + x] + bayer[y & 3][x & 3]) >> 4;
t->nv12[(size_t)y * w + x] = (uint8_t)(v > 255 ? 255 : v);
}
uint8_t *uv = t->nv12 + (size_t)w * t->h;
for (int y = t->y0 / 2; y < t->y1 / 2; y++)
for (int x = 0; x < w / 2; x++) {
int d = bayer[y & 3][x & 3], i = y * (w / 2) + x;
int u = (t->U16[i] + d) >> 4, v = (t->V16[i] + (15 - d)) >> 4;
uv[(size_t)y * w + 2 * x] = (uint8_t)(u > 255 ? 255 : u);
uv[(size_t)y * w + 2 * x + 1] = (uint8_t)(v > 255 ? 255 : v);
}
}
static void stripe_run(void *arg) {
Stripe *t = arg;
if (t->phase == 0) {
const uint8_t *uv = t->nv12 + (size_t)t->w * t->h;
deband(t->nv12, t->Y16, t->w, t->h, t->w, 1, t->y0, t->y1);
deband(uv, t->U16, t->w / 2, t->h / 2, t->w, 2, t->y0 / 2, t->y1 / 2);
deband(uv + 1, t->V16, t->w / 2, t->h / 2, t->w, 2, t->y0 / 2, t->y1 / 2);
} else if (t->phase == 2) { // deband straight from the decoder's planes, so the NV12 rows can be written at once
deband(t->py, t->Y16, t->w, t->h, t->ys, 1, t->y0, t->y1);
deband(t->pu, t->U16, t->w / 2, t->h / 2, t->cs, 1, t->y0 / 2, t->y1 / 2);
deband(t->pv, t->V16, t->w / 2, t->h / 2, t->cs, 1, t->y0 / 2, t->y1 / 2);
to_nv12(t);
} else if (t->rgba) {
to_rgba(t->Y16, t->U16, t->V16, t->rgba, t->w, t->h, t->y0, t->y1);
} else {
to_nv12(t);
}
}
// The NSTRIPES - 1 helper threads are made once and kept. Made per call (two calls per picture, 30-60 pictures a
// second) they were about 300 new threads a second for as long as a movie played, and every DLL in the process (the
// GPU driver's, overlays and audio hooks) sets itself up for each new thread: a cost per thread, and with an AMD driver
// on an RX 580 the process commit grew by about 300 MB a second during the title movie until the system ran out
// (FATAL "movie: out of memory for a 1920x1080 frame" at 45.7 GB). One caller at a time (call_lock); a worker that
// could not be started leaves its stripe to the caller.
static struct {
PlatMutex call_lock; // one run_stripes at a time (several decoders may exist)
PlatMutex lock; // the fields below
PlatCond go, done;
Stripe job[NSTRIPES];
uint64_t gen; // bumped per batch
int pending; // helper stripes of the batch not finished
int nworkers; // started helpers (stripes 1..nworkers); -1 not tried yet
} SP = {PLAT_MUTEX_INIT, PLAT_MUTEX_INIT, PLAT_COND_INIT, PLAT_COND_INIT, {{0}}, 0, 0, -1};
static void stripe_worker(void *arg) {
int i = (int)(intptr_t)arg;
uint64_t seen = 0;
plat_mutex_lock(&SP.lock);
for (;;) {
while (SP.gen == seen) plat_cond_wait(&SP.go, &SP.lock);
seen = SP.gen;
Stripe t = SP.job[i];
plat_mutex_unlock(&SP.lock);
stripe_run(&t);
plat_mutex_lock(&SP.lock);
if (!--SP.pending) plat_cond_broadcast(&SP.done);
}
}
static void run_stripes(Stripe base) {
plat_mutex_lock(&SP.call_lock);
if (SP.nworkers < 0) {
SP.nworkers = 0;
for (int i = 1; i < NSTRIPES; i++, SP.nworkers++)
if (!plat_thread_spawn(stripe_worker, (void *)(intptr_t)i, "movie convert", 0)) break;
if (SP.nworkers < NSTRIPES - 1) rt_log("movie: %d of %d convert threads started", SP.nworkers, NSTRIPES - 1);
}
int rows = ((base.h + NSTRIPES - 1) / NSTRIPES + 1) & ~1; // even: chroma rows split at the same places
Stripe st[NSTRIPES];
for (int i = 0; i < NSTRIPES; i++) {
st[i] = base;
st[i].y0 = i * rows < base.h ? i * rows : base.h;
st[i].y1 = (i + 1) * rows < base.h ? (i + 1) * rows : base.h;
}
plat_mutex_lock(&SP.lock);
memcpy(SP.job, st, sizeof st);
SP.pending = SP.nworkers;
SP.gen++;
plat_cond_broadcast(&SP.go);
plat_mutex_unlock(&SP.lock);
stripe_run(&st[0]);
for (int i = SP.nworkers + 1; i < NSTRIPES; i++) stripe_run(&st[i]); // helpers that could not start
plat_mutex_lock(&SP.lock);
while (SP.pending) plat_cond_wait(&SP.done, &SP.lock);
plat_mutex_unlock(&SP.lock);
plat_mutex_unlock(&SP.call_lock);
}
// Out of memory for a decoded frame: the frame is dropped (the game shows the previous one a little longer) and
// the first drop of a run is logged. The game used to stop here; on a PC whose commit charge reached its limit
// for a moment, that ended a run that the next second would have had memory for.
static void movie_oom_log(int w, int h) {
static volatile int32_t drops;
if (plat_xchg32(&drops, 1) == 0) rt_log("movie: out of memory for a %dx%d frame: dropped (later drops not logged)", w, h);
}
// The Video's deband planes for a w x h picture (convert thread). 0: no memory for them; the frame is then dropped
// as for any allocation of a picture, and no planes are kept.
static int planes_for(Video *v, int w, int h) {
size_t px = (size_t)w * h;
if (px <= v->plane_room) return 1;
free(v->y16), free(v->u16), free(v->v16);
v->y16 = malloc(px * 2), v->u16 = malloc(px / 2), v->v16 = malloc(px / 2);
v->plane_room = px;
if (v->y16 && v->u16 && v->v16) return 1;
free(v->y16), free(v->u16), free(v->v16);
v->y16 = v->u16 = v->v16 = NULL;
v->plane_room = 0;
return 0;
}
// One dav1d picture (8- or 10-bit 4:2:0) -> NV12 with 8-bit samples, and its RGBA; convert thread
static int make_picture(Video *v, const Dav1dPicture *pic, Picture *p) {
int w = pic->p.w, h = pic->p.h, hbd = pic->p.bpc > 8, sh = pic->p.bpc - 8;
if (pic->p.layout != DAV1D_PIXEL_LAYOUT_I420) {
static volatile int32_t logged;
if (!plat_xchg32(&logged, 1)) rt_log("movie: picture layout %d is not 4:2:0: skipped", pic->p.layout);
return 0;
}
{
static volatile int32_t logged;
if (!plat_xchg32(&logged, 1) && pic->seq_hdr)
rt_log("movie: stream %dx%d, %d-bit, colour range %s, matrix %d, primaries %d, transfer %d", w, h,
pic->p.bpc, pic->seq_hdr->color_range ? "full" : "limited", pic->seq_hdr->mtrx, pic->seq_hdr->pri,
pic->seq_hdr->trc);
}
// The host overlay (NX2NAVI_MOVIE_OVERLAY=1) takes an RGBA frame; without it the game converts the NV12 frame itself.
static int overlay = -1;
if (overlay < 0) {
const char *e = cfg_get("NX2NAVI_MOVIE_OVERLAY");
overlay = e && strcmp(e, "0");
}
uint8_t *o = malloc((size_t)w * h * 3 / 2), *rgba = overlay ? malloc((size_t)w * h * 4) : NULL;
if (!o || (overlay && !rgba)) { // a frame the PC has no memory for is dropped, not the game
free(o), free(rgba);
movie_oom_log(w, h);
return 0;
}
const ptrdiff_t ys = pic->stride[0], cs = pic->stride[1];
static int deband_on = -1; // NX2NAVI_MOVIE_DEBAND=0 keeps the decoded planes
if (deband_on < 0) {
const char *e = cfg_get("NX2NAVI_MOVIE_DEBAND");
deband_on = !(e && !strcmp(e, "0"));
}
if (deband_on && !hbd && !rgba) {
// The usual case (8-bit, debanded, the game converts the frame): one pass per stripe debands the decoder's own
// planes and writes the dithered NV12 rows. Reading the decoder's planes instead of an NV12 copy of them gives
// the same samples, and nothing reads the frame while it is written, so each stripe finishes its rows alone:
// one round of threads per frame instead of two, and no single-threaded copy before it.
if (!planes_for(v, w, h)) {
free(o);
movie_oom_log(w, h);
return 0;
}
Stripe base = {2, 0, 0, w, h, o, v->y16, v->u16, v->v16, NULL, pic->data[0], pic->data[1], pic->data[2],
ys, cs};
run_stripes(base);
*p = (Picture){o, NULL, w, h, pic->m.timestamp, 0};
return 1;
}
for (int y = 0; y < h; y++) {
const uint8_t *row = (const uint8_t *)pic->data[0] + y * ys;
if (!hbd) memcpy(o + (size_t)y * w, row, (size_t)w);
else for (int x = 0; x < w; x++) o[(size_t)y * w + x] = (uint8_t)(((const uint16_t *)row)[x] >> sh);
}
uint8_t *uv = o + (size_t)w * h;
for (int y = 0; y < h / 2; y++) {
const uint8_t *u = (const uint8_t *)pic->data[1] + y * cs, *vv = (const uint8_t *)pic->data[2] + y * cs;
for (int x = 0; x < w / 2; x++) {
uv[(size_t)y * w + 2 * x] = hbd ? (uint8_t)(((const uint16_t *)u)[x] >> sh) : u[x];
uv[(size_t)y * w + 2 * x + 1] = hbd ? (uint8_t)(((const uint16_t *)vv)[x] >> sh) : vv[x];
}
}
// debanded planes (NX2NAVI_MOVIE_DEBAND=0 keeps the decoded ones), then the overlay's RGBA or the dithered NV12
if (!deband_on && !rgba) {
*p = (Picture){o, NULL, w, h, pic->m.timestamp, 0};
return 1;
}
{
if (!planes_for(v, w, h)) {
free(o), free(rgba);
movie_oom_log(w, h);
return 0;
}
uint16_t *Y16 = v->y16, *U16 = v->u16, *V16 = v->v16;
Stripe base = {0, 0, 0, w, h, o, Y16, U16, V16, rgba};
if (deband_on) {
run_stripes(base);
} else {
for (size_t i = 0; i < (size_t)w * h; i++) Y16[i] = (uint16_t)(o[i] * 16);
for (size_t i = 0; i < (size_t)w * h / 4; i++) U16[i] = (uint16_t)(uv[2 * i] * 16), V16[i] = (uint16_t)(uv[2 * i + 1] * 16);
}
base.phase = 1;
run_stripes(base);
}
*p = (Picture){o, rgba, w, h, pic->m.timestamp, 0};
return 1;
}
// Queues a decoded picture, waiting while the queue is full (the game acquires at the movie's pace); decoder thread
static void deliver(Video *v, Picture p, uint64_t gen) {
plat_mutex_lock(&v->lock);
while (v->qn == QMAX && !v->quit && v->generation == gen) {
plat_mutex_unlock(&v->lock);
plat_sleep_ms(2);
plat_mutex_lock(&v->lock);
}
if (v->quit || v->generation != gen) {
plat_mutex_unlock(&v->lock);
picture_free(&p);
return;
}
v->q[(v->qh + v->qn) % QMAX] = p;
v->qn++;
v->decoded += !p.eos;
update_events(v);
plat_mutex_unlock(&v->lock);
}
// Queues a decoded picture (or the end-of-stream marker) for the convert thread, waiting while it has CQMAX;
// decoder thread. On quit the picture is released here.
static void hand_over(Video *v, Decoded d) {
plat_mutex_lock(&v->lock);
while (v->cqn == CQMAX && !v->quit) plat_cond_wait(&v->conv, &v->lock);
if (v->quit) {
plat_mutex_unlock(&v->lock);
if (!d.eos) dav1d_picture_unref(&d.pic);
return;
}
v->cq[(v->cqh + v->cqn) % CQMAX] = d;
v->cqn++;
plat_cond_broadcast(&v->conv);
plat_mutex_unlock(&v->lock);
}
// Releases the pictures still waiting for the convert thread; under v->lock
static void clear_converts(Video *v) {
while (v->cqn) {
Decoded *d = &v->cq[v->cqh];
if (!d->eos) dav1d_picture_unref(&d->pic);
v->cqh = (v->cqh + 1) % CQMAX;
v->cqn--;
}
v->cqh = 0;
plat_cond_broadcast(&v->conv);
}
static void convert_thread(void *arg) {
Video *v = arg;
plat_mutex_lock(&v->lock);
for (;;) {
while (!v->cqn && !v->quit) plat_cond_wait(&v->conv, &v->lock);
if (v->quit) break;
Decoded d = v->cq[v->cqh];
v->cqh = (v->cqh + 1) % CQMAX;
v->cqn--;
plat_cond_broadcast(&v->conv);
plat_mutex_unlock(&v->lock);
if (d.eos) {
deliver(v, (Picture){NULL, NULL, 0, 0, d.ts, 1}, d.gen);
} else {
uint64_t t0 = plat_now_ns();
Picture p;
int ok = make_picture(v, &d.pic, &p);
t_make_ns += plat_now_ns() - t0;
n_made++;
dav1d_picture_unref(&d.pic);
if (ok) deliver(v, p, d.gen);
}
plat_mutex_lock(&v->lock);
}
plat_mutex_unlock(&v->lock);
}
static void drain(Video *v, uint64_t gen) {
Dav1dPicture pic = {0};
for (;;) {
uint64_t t0 = plat_now_ns();
int got = dav1d_get_picture(v->dav1d, &pic) == 0;
uint64_t t1 = plat_now_ns();
t_get_ns += t1 - t0;
if (!got) break;
hand_over(v, (Decoded){pic, gen, pic.m.timestamp, 0});
pic = (Dav1dPicture){0};
}
}
static void decoder_thread(void *arg) {
Video *v = arg;
plat_mutex_lock(&v->lock);
for (;;) {
uint64_t tw = plat_now_ns();
while (!v->in_n && !v->quit) plat_cond_wait(&v->work, &v->lock);
t_wait_ns += plat_now_ns() - tw;
if (v->quit) break;
Sample s = v->in[v->ih];
v->ih = (v->ih + 1) % INMAX;
v->in_n--;
uint64_t gen = v->generation;
update_events(v);
plat_mutex_unlock(&v->lock);
plat_mutex_lock(&v->codec);
// a flush (ov_flush) between taking the sample and the codec: the sample belongs to the stream before it, and fed
// into the flushed decoder its picture would come out after the seek
plat_mutex_lock(&v->lock);
int stale = v->generation != gen;
plat_mutex_unlock(&v->lock);
if (stale) s.size = 0, s.eos = 0;
if (v->dav1d && s.data && s.size) {
Dav1dData d = {0};
uint8_t *p = dav1d_data_create(&d, s.size);
if (p) {
memcpy(p, s.data, s.size);
d.m.timestamp = s.ts;
while (d.sz) {
uint64_t ts0 = plat_now_ns();
int r = dav1d_send_data(v->dav1d, &d);
t_send_ns += plat_now_ns() - ts0;
if (r == DAV1D_ERR(EAGAIN)) drain(v, gen);
else if (r < 0) {
static volatile int32_t logged;
if (!plat_xchg32(&logged, 1)) rt_log("movie: dav1d rejected a sample (%d)", r);
dav1d_data_unref(&d);
break;
}
}
drain(v, gen);
}
}
if (s.eos && v->dav1d) {
drain(v, gen);
hand_over(v, (Decoded){.gen = gen, .ts = s.ts, .eos = 1}); // after the last picture, through the same queue
}
plat_mutex_unlock(&v->codec);
free(s.data);
plat_mutex_lock(&v->lock);
}
plat_mutex_unlock(&v->lock);
}
static void stop_thread(Video *v) {
if (!v->thread && !v->convert) return;
plat_mutex_lock(&v->lock);
v->quit = 1;
plat_cond_broadcast(&v->work);
plat_cond_broadcast(&v->conv);
plat_mutex_unlock(&v->lock);
if (v->thread) plat_thread_join(v->thread);
if (v->convert) plat_thread_join(v->convert);
v->thread = v->convert = NULL;
plat_mutex_lock(&v->lock);
clear_converts(v); // before dav1d_close: the pictures are the decoder's
v->quit = 0;
plat_mutex_unlock(&v->lock);
}
static void finalize(Video *v) {
stop_thread(v);
free(v->y16), free(v->u16), free(v->v16); // the convert thread, their only user, has been joined
v->y16 = v->u16 = v->v16 = NULL;
v->plane_room = 0;
plat_mutex_lock(&v->codec);
if (v->dav1d) dav1d_close(&v->dav1d);
plat_mutex_unlock(&v->codec);
plat_mutex_lock(&v->lock);
clear_queues(v);
memset(v->busy, 0, sizeof v->busy);
plat_mutex_unlock(&v->lock);
}
// ---- movie::VideoDecoder
// VideoDecoder() / ~VideoDecoder(): the object's first 8 bytes hold the host record
static void ov_ctor(Cpu *c) { *(Video **)arg_p(c, 0) = NULL; }
static void ov_dtor(Cpu *c) {
Video **slot = arg_p(c, 0);
if (!slot || !*slot) return;
Video *v = *slot;
finalize(v);
set_live(v, 0);
// The decoder and convert threads are joined, so nothing signals the events now: they are destroyed and leave the
// kernel's table of system events before their memory goes (kernel.c kern_unbind_system_event). Kept, each
// decoder left two dead entries and their kernel handles, and a long session filled the table.
if (v->in_bound) kern_unbind_system_event(c, (uint64_t)(uintptr_t)v->in_ev);
if (v->out_bound) kern_unbind_system_event(c, (uint64_t)(uintptr_t)v->out_ev);
free(v);
*slot = NULL;
gfx_movie_stop();
}
void vk_dump_movie_frame(int k, uint64_t shown); // vk_dump.c (NX2NAVI_DUMP_MOVIE)
static int movies_started; // decoders that came up this run (Video.run_index)
// Result Initialize(void *memory, size_t size, MediaMimeType mime, VideoDecoderOutputFormat format, VideoCodecMode)
static void ov_initialize(Cpu *c) {
Video **slot = arg_p(c, 0);
if (!*slot) {
*slot = calloc(1, sizeof **slot);
if (!*slot) rt_fatal("movie: decoder allocation");
plat_mutex_init(&(*slot)->lock);
plat_mutex_init(&(*slot)->codec);
plat_cond_init(&(*slot)->work);
plat_cond_init(&(*slot)->conv);
}
Video *v = *slot;
finalize(v);
set_live(v, 1);
v->clock_on = v->rel_on = 0;
Dav1dSettings s;
dav1d_default_settings(&s);
// Frame threading on (dav1d picks the delay): heavy stretches of the title movie took longer than a frame to
// decode one picture at a time (max_frame_delay 1) and the game's loop waited up to 94 ms for them. The delay
// costs nothing here, as decoding runs ahead on its own thread; end of stream drains the delayed pictures.
s.n_threads = 8;
s.max_frame_delay = 0;
int r = dav1d_open(&v->dav1d, &s);
if (r < 0) rt_fatal("movie: dav1d_open failed (%d)", r);
v->thread = plat_thread_new(decoder_thread, v, "movie decoder");
if (!v->thread) rt_fatal("movie: cannot start the decoder thread");
v->convert = plat_thread_new(convert_thread, v, "movie convert");
if (!v->convert) rt_fatal("movie: cannot start the convert thread");
v->run_index = __atomic_add_fetch(&movies_started, 1, __ATOMIC_RELAXED);
rt_log("movie: video decoder %d up (AV1 via dav1d %s on its own thread; mime %u, output format %u, mode %u)",
v->run_index, dav1d_version(), (unsigned)arg_x(c, 3), (unsigned)arg_x(c, 4), (unsigned)arg_x(c, 5));
ret_x(c, 0);
}
// Result Configure(const MediaProperty *, const void *configuration, size_t): the AV1 sequence header also comes
// in band with the first key frame, which dav1d reads
static void ov_ok(Cpu *c) { ret_x(c, 0); }
// Result Finalize()
static void ov_finalize(Cpu *c) {
Video *v = video_of(arg_p(c, 0));
if (v) {
rt_log("movie: video decoder finalised (%llu frames decoded, %llu shown)", (unsigned long long)v->decoded,
(unsigned long long)v->shown);
finalize(v);
set_live(v, 0);
}
gfx_movie_stop();
ret_x(c, 0);
}
// Result RegisterVideoFrame(VideoFrame **frames, int count)
static void ov_register(Cpu *c) {
Video *v = video_of(arg_p(c, 0));
uint64_t *frames = arg_p(c, 1);
int n = (int)arg_x(c, 2);
if (!v || n < 0 || n > NFRAMES || (n && !frames)) rt_fatal("movie: RegisterVideoFrame of %d frames", n);
plat_mutex_lock(&v->lock);
memcpy(v->frame, frames, (size_t)n * sizeof *frames);
memset(v->busy, 0, sizeof v->busy);
v->nframe = n;
// Each frame buffer starts as NV12 black (Y 16, UV 128 in its last third). The game converts a frame it holds
// before a picture arrives; zeros there are YUV (0, 0, 0), green, in stripes where rows were partly written.
for (int i = 0; i < n; i++) {
uint8_t *fr = (uint8_t *)(uintptr_t)frames[i], *buf;
uint64_t cap;
if (!fr) continue;
memcpy(&buf, fr + 0x08, 8), memcpy(&cap, fr + 0x48, 8);
if (!buf || cap < 3 || cap > (64u << 20)) continue;
uint64_t ysz = cap / 3 * 2;
memset(buf, 16, (size_t)ysz);
memset(buf + ysz, 128, (size_t)(cap - ysz));
}
plat_mutex_unlock(&v->lock);
ret_x(c, 0);
}
// Result GetInputReadyEvent(SystemEventType **out) / GetOutputReadyEvent(SystemEventType **out)
static void get_event(Cpu *c, int input) {
Video *v = video_of(arg_p(c, 0));
uint64_t *out = arg_p(c, 1);
if (!v || !out) rt_fatal("movie: Get%sReadyEvent before Initialize", input ? "Input" : "Output");
uint8_t *ev = input ? v->in_ev : v->out_ev;
int *bound = input ? &v->in_bound : &v->out_bound;
if (!*bound) {
kern_bind_system_event(c, (uint64_t)(uintptr_t)ev, 0);
plat_mutex_lock(&v->lock);
*bound = 1;
plat_mutex_unlock(&v->lock);
}
plat_mutex_lock(&v->lock);
update_events(v);
plat_mutex_unlock(&v->lock);
*out = (uint64_t)(uintptr_t)ev;
ret_x(c, 0);
}
static void ov_input_event(Cpu *c) { get_event(c, 1); }
static void ov_output_event(Cpu *c) { get_event(c, 0); }
// Result Flush(): samples and pictures not yet acquired are dropped (a seek or a restart follows)
static void ov_flush(Cpu *c) {
Video *v = video_of(arg_p(c, 0));
if (v) {
plat_mutex_lock(&v->lock);
v->generation++; // the decoder and convert threads drop what they are making
v->clock_on = v->rel_on = 0; // a seek: the report's real-time reference starts again from the next picture
clear_queues(v);
clear_converts(v);
plat_mutex_unlock(&v->lock);
plat_mutex_lock(&v->codec);
if (v->dav1d) dav1d_flush(v->dav1d);
plat_mutex_unlock(&v->codec);
plat_mutex_lock(&v->lock);
update_events(v);
plat_mutex_unlock(&v->lock);
}
ret_x(c, 0);
}
// Result DecodeAsync(const nn::movie::Buffer *sample): queued for the decoder thread
static void ov_decode(Cpu *c) {
Video *v = video_of(arg_p(c, 0));
const uint8_t *b = arg_p(c, 1);
if (!v || !v->dav1d || !b) {
ret_x(c, 0);
return;
}
const uint8_t *data;
uint64_t size;
int64_t ts;
uint32_t flags;
memcpy(&data, b, 8), memcpy(&size, b + 0x10, 8), memcpy(&ts, b + 0x18, 8), memcpy(&flags, b + 0x20, 4);
int eos = (flags & BUFFER_FLAG_EOS) != 0;
static int64_t skip_ns = -2; // NX2NAVI_MOVIE_SKIP: -1 off
if (skip_ns == -2) {
const char *e = cfg_get("NX2NAVI_MOVIE_SKIP");
skip_ns = e && *e ? (int64_t)(atof(e) * 1e9) : -1;
if (skip_ns >= 0) rt_log("movie: movies end after %.1f s (NX2NAVI_MOVIE_SKIP)", skip_ns / 1e9);
}
plat_mutex_lock(&v->lock);
// Finalize on another thread may have closed the decoder since the test above. finalize takes v->lock after it
// clears v->dav1d, so read here under v->lock it is either still set (the sample is queued before finalize's
// clear_queues, which drops it, and ov_finalize or the destructor then clears live) or already NULL: no sample
// makes a finalized decoder live again, which would hold the movie frame rate cap (frame_rate.c movie_cap_hz).
if (!v->dav1d) {
plat_mutex_unlock(&v->lock);
ret_x(c, 0);
return;
}
if (skip_ns >= 0 && v->eos_queued) { // after the early end: what the demuxer still sends is dropped
plat_mutex_unlock(&v->lock);
ret_x(c, 0);
return;
}
if (skip_ns >= 0 && !eos && ts >= skip_ns) eos = 1, size = 0; // the early end
if (v->in_n >= INMAX || ((v->qn >= QREADY || v->in_n >= QREADY) && !eos)) {
update_events(v);
plat_mutex_unlock(&v->lock);
ret_x(c, R_BUSY);
return;
}
Sample s = {NULL, 0, ts, eos};
if (data && size) {
s.data = malloc((size_t)size);
if (!s.data) { // no memory for the sample now: the game is told to try again, as with a full queue
static volatile int32_t logged;
if (plat_xchg32(&logged, 1) == 0)
rt_log("movie: out of memory for a %llu-byte sample: retried (later ones not logged)", (unsigned long long)size);
update_events(v);
plat_mutex_unlock(&v->lock);
ret_x(c, R_BUSY);
return;
}
memcpy(s.data, data, (size_t)size);
s.size = (size_t)size;
}
v->in[(v->ih + v->in_n) % INMAX] = s;
v->in_n++;
if (!eos && !v->eos_queued) set_live(v, 1); // playing again after its end (a Flush clears eos_queued)
if (eos) v->eos_queued = 1;
update_events(v);
plat_cond_signal(&v->work);
plat_mutex_unlock(&v->lock);
ret_x(c, 0);
}
// Result AcquireVideoFrame(VideoFrame **out): the oldest decoded picture in a free registered frame
static void ov_acquire(Cpu *c) {
Video *v = video_of(arg_p(c, 0));
uint64_t *out = arg_p(c, 1);
if (!v || !out) {
ret_x(c, R_NO_FRAME);
return;
}
plat_mutex_lock(&v->lock);
int f = 0;
while (f < v->nframe && v->busy[f]) f++;
if (!v->qn || f == v->nframe) {
plat_mutex_unlock(&v->lock);
ret_x(c, R_NO_FRAME);
return;
}
Picture p = v->q[v->qh];
v->qh = (v->qh + 1) % QMAX;
v->qn--;
uint8_t *fr = (uint8_t *)(uintptr_t)v->frame[f];
uint8_t *buf;
uint64_t cap;
memcpy(&buf, fr + 0x08, 8), memcpy(&cap, fr + 0x48, 8);
uint64_t ysz = (uint64_t)p.w * p.h, uvsz = ysz / 2;
if (p.nv12 && buf && cap >= ysz + uvsz) {
memcpy(buf, p.nv12, (size_t)(ysz + uvsz));
// The game's YUV pass reads this buffer on the GPU. A host write into pool storage is one the page write
// tracking may not see in time (a mirror or texture over it kept its older or still-empty bytes: frames
// with stripes of zeros, which NV12 shows as green), so the back end is told directly.
nvn_guest_written((uint64_t)(uintptr_t)buf, ysz + uvsz);
uint64_t uvp = (uint64_t)(uintptr_t)(buf + ysz);
memcpy(fr + 0x10, &uvp, 8);
memcpy(fr + 0x28, &ysz, 8), memcpy(fr + 0x30, &uvsz, 8);
} else {
uint64_t zero = 0;
memcpy(fr + 0x28, &zero, 8), memcpy(fr + 0x30, &zero, 8);
if (p.nv12) {
static volatile int32_t logged;
if (!plat_xchg32(&logged, 1))
rt_log("movie: frame buffer %#llx bytes is smaller than a %dx%d NV12 frame", (unsigned long long)cap,
p.w, p.h);
}
}
memcpy(fr + 0x78, &p.ts, 8);
fr[0x88] = (uint8_t)p.eos;
v->busy[f] = 1;
v->shown += !p.eos;
if (p.nv12 && v->shown == 1) rt_log("movie: first frame %dx%d shown", p.w, p.h);
if (p.nv12) vk_dump_movie_frame(v->run_index, v->shown);
if (p.nv12 && !v->clock_on) v->ts0 = p.ts, v->shown0_ns = plat_now_ns(), v->clock_on = 1;
if (p.nv12) { // pacing report every 600 frames (10 s): the gaps between acquisitions and their time stamps
static uint64_t last_ns, sum_gap, max_gap, n;
uint64_t now = plat_now_ns(), gap = last_ns ? now - last_ns : 0;
// how far the picture is behind the time since the first one (positive: the picture is late)
int64_t lag = (int64_t)(now - v->shown0_ns) - (p.ts - v->ts0);
sum_gap += gap, n++;
if (gap > max_gap) max_gap = gap;
if (n % 600 == 0) {
rt_log("movie: pacing over 600 frames: acquire gap avg %.2f ms, max %.2f ms; %d pictures waiting; "
"per frame shown: decoder thread waiting for samples %.2f ms, send %.2f ms, get %.2f ms; convert thread "
"%.2f ms per picture (%llu pictures); "
"picture %.0f ms behind real time since the first picture; on screen %.0f ms behind, %+.0f ms since its start",
sum_gap / 600 / 1e6, max_gap / 1e6, v->qn, t_wait_ns / 600 / 1e6, t_send_ns / 600 / 1e6,
t_get_ns / 600 / 1e6, n_made ? t_make_ns / n_made / 1e6 : 0.0, (unsigned long long)n_made, lag / 1e6, v->rel_lag / 1e6,
(v->rel_lag - v->rel_lag0) / 1e6);
t_wait_ns = t_send_ns = t_get_ns = t_make_ns = n_made = 0;
sum_gap = max_gap = 0;
}
last_ns = now;
}
update_events(v);
plat_mutex_unlock(&v->lock);
if (p.rgba) gfx_movie_frame_own(p.rgba, p.w, p.h), p.rgba = NULL; // the overlay takes the buffer
if (p.eos) { // how the movie kept time: the picture on screen against the clock, at its start and its end
if (v->rel_on)
rt_log("movie: end of stream after %llu pictures: on screen %.0f ms behind real time, %+.0f ms since its start",
(unsigned long long)v->shown, v->rel_lag / 1e6, (v->rel_lag - v->rel_lag0) / 1e6);
gfx_movie_stop(), set_live(v, 0);
}
picture_free(&p);
*out = v->frame[f];
ret_x(c, 0);
}
// Result ReleaseVideoFrame(VideoFrame *)
static void ov_release(Cpu *c) {
Video *v = video_of(arg_p(c, 0));
uint64_t fr = arg_x(c, 1);
if (v) {
plat_mutex_lock(&v->lock);
for (int i = 0; i < v->nframe; i++)
if (v->frame[i] == fr) {
v->busy[i] = 0;
int64_t ts;
memcpy(&ts, (uint8_t *)(uintptr_t)fr + 0x78, 8);
if (v->clock_on && !((uint8_t *)(uintptr_t)fr)[0x88]) {
v->rel_lag = (int64_t)(plat_now_ns() - v->shown0_ns) - (ts - v->ts0);
if (!v->rel_on) v->rel_lag0 = v->rel_lag, v->rel_on = 1;
}
}
update_events(v);
plat_mutex_unlock(&v->lock);
}
ret_x(c, 0);
}
// Skipping movies (NX2NAVI_MOVIES=off) leaves the subsdk0 decoder in place: the movie never opens, so it is not called.
#define S(n) "_ZN5movie12VideoDecoder" n
HOST_OVERRIDE(S("C1Ev"), ov_ctor);
HOST_OVERRIDE(S("D1Ev"), ov_dtor);
HOST_OVERRIDE(S("10InitializeEPvmN2nn5movie13MediaMimeTypeENS_24VideoDecoderOutputFormatENS_14VideoCodecModeE"),
ov_initialize);
HOST_OVERRIDE(S("9ConfigureEPKN2nn5movie13MediaPropertyEPKvm"), ov_ok);
HOST_OVERRIDE(S("8FinalizeEv"), ov_finalize);
HOST_OVERRIDE(S("18RegisterVideoFrameEPPNS_10VideoFrameEi"), ov_register);
HOST_OVERRIDE(S("18GetInputReadyEventEPPN2nn2os15SystemEventTypeE"), ov_input_event);
HOST_OVERRIDE(S("19GetOutputReadyEventEPPN2nn2os15SystemEventTypeE"), ov_output_event);
HOST_OVERRIDE(S("5StartEv"), ov_ok);
HOST_OVERRIDE(S("4StopEv"), ov_ok);
HOST_OVERRIDE(S("5FlushEv"), ov_flush);
HOST_OVERRIDE(S("11DecodeAsyncEPKN2nn5movie6BufferE"), ov_decode);
HOST_OVERRIDE(S("17AcquireVideoFrameEPPNS_10VideoFrameE"), ov_acquire);
HOST_OVERRIDE(S("17ReleaseVideoFrameEPNS_10VideoFrameE"), ov_release);
#undef S