Files
ProSepandClaude Opus 5.5 3cd241071a Check the movie picture fit against gradients and make it cheaper
The probe now feeds gradient planes and checks bar symmetry, the picture's shape on screen, black outside the picture
and the expected luma and separate U and V samples, with one cleanup path. The timing loop runs only with
TOTK_PROBE_MOVIE_BENCH. In the decoder, a plan made for another frame size is never used, an allocation failure is
logged, only the bar strips are filled with black, and the 3 and 4 tap filters are unrolled, which halves the cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:41:10 +01:00

1068 lines
45 KiB
C

// Adapted from MacNativeGames games/totk/runtime/movie_host.c @ b75be8cf (MIT, Copyright (c) 2026 MacNativeGames
// contributors).
// movie::Decoder for VP9 on the host with libvpx (BSD-3-Clause, linked statically).
// TotK's pre-rendered movies (Event/Movie/*.webm, 28 files: VP9 profile 0, 1920x1080, 30 fps, video only) play through
// the SDK movie library in subsdk0, Android's MediaCodec over NVIDIA NvMM. Its decoder opens NVDEC through NvRm, whose
// first NvOsDrvOpen (sdk+0x3f9050) aborts with "Use of nv::InitializeGraphics is required." because ov_gfx.c takes
// nv::InitializeGraphics over; nvdrv and mm:u are not served either. The game's own demuxer (movie::Extractor) runs as
// guest code; only the decoder is replaced.
// main imports the constructor and calls the decoder through the vtable for movie::Decoder, whose slots are ABS64
// relocations by name: the HOST_OVERRIDE entries below replace them. A VP9 buffer-mode decoder (type 3, mode 1) is a host
// object, +0 the library's vtable, +8 the host Movie (where the library keeps its DecoderImpl); any other decoder runs
// the original code. Behaviour follows DecoderImpl:
// - output format 4 is packed NV12 (Y plane, then interleaved UV; stride = width), copied into the client's Buffer;
// - input and output buffers are indices handed out FIFO, as MediaCodec does; input flag 4 / output flag 4 = end of stream;
// - client events {input available, output available, format changed, error} (DecoderEvents, 4 EventType pointers) are
// signalled with nn::os::SignalEvent on a guest thread.
// Decoding runs on one host worker thread per decoder (libvpx adds its own tile threads), never on the game's threads:
// SendInputBufferForDecode queues the input and returns. Events the worker owes (input free, format changed, error) are
// signalled at the next entry call or present, whichever comes first.
// Pacing: the game shows one decoded frame per game frame, which plays a 30 fps movie at 30 fps only at 30 fps. So
// decoded frames are handed over when their presentation time is due, counted from the first frame (movie_tick, at every
// present on the presenting guest thread), at most one per present. When the game falls behind, a frame more than 1.5
// periods late is dropped if the next one is due too, so the picture keeps time with the movie's sound.
// TOTK_MOVIE_PACE=0 hands every decoded frame over at the next present; TOTK_MOVIE_HOST=0 leaves every decoder to the
// library (which aborts at the first movie).
#include <emmintrin.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <vpx/vp8dx.h>
#include <vpx/vpx_decoder.h>
#include "cfg.h"
#include "host.h"
#include "movie.h"
#include "platform/platform.h"
enum { IN_BUFS = 4, IN_CAP = 2 << 20, OUT_BUFS = 8, MAX_MOVIES = 16, VPX_THREADS = 4 };
enum { ST_OK = 0, ST_ERROR = 0x1000, ST_NULL = 0x100b, ST_NO_OUTPUT = 0x100e, ST_NO_INPUT = 0x1011,
ST_UNSUPPORTED = 0x2001, ST_NOT_CONFIGURED = 0x2002 };
const char *const movie_symbols[MOVIE_ENTRIES] = {
"_ZN5movie7DecoderC1ENS_11DecoderTypeENS_19DecoderOutputFormatENS_11DecoderModeEi", "_ZN5movie7DecoderD2Ev",
"_ZN5movie7DecoderD0Ev", "_ZN5movie7Decoder9ConfigureEPNS_9MediaDataE",
"_ZN5movie7Decoder20RegisterClientEventsEPNS_13DecoderEventsE", "_ZN5movie7Decoder15GetOutputFormatEPNS_9MediaDataE",
"_ZN5movie7Decoder5StartEv", "_ZN5movie7Decoder4StopEv", "_ZN5movie7Decoder5ResetEv", "_ZN5movie7Decoder5FlushEv",
"_ZN5movie7Decoder23AcquireInputBufferIndexEPiS1_", "_ZN5movie7Decoder14GetInputBufferEiPNS_6BufferE",
"_ZN5movie7Decoder24SendInputBufferForDecodeEiiilj", "_ZN5movie7Decoder24AcquireOutputBufferIndexEPiPlPjS1_",
"_ZN5movie7Decoder15GetOutputBufferEiPNS_6BufferE", "_ZN5movie7Decoder19GetOutputBufferSizeEPiS1_",
"_ZN5movie7Decoder24ReleaseOutputBufferIndexEi", "_ZN5movie7Decoder12GetLastErrorEv",
"_ZN5movie7Decoder15GetOutputBufferEiPNS_6BufferEPNS_9MediaDataE",
"_ZN5movie7Decoder21GetOutputBufferFormatEiPNS_9MediaDataE"};
typedef struct { int index; unsigned flags; int64_t time; } Ready;
typedef struct { int index, offset, size; int64_t time; unsigned flags; } Pending;
typedef struct {
uint64_t guest; // the movie::Decoder object
int format, configured, started, w, h, eos_in, format_sent, last_error;
uint64_t events; // guest DecoderEvents *: EventType * per MOVIE_EV_*
vpx_codec_ctx_t vpx;
int vpx_live;
uint8_t *in[IN_BUFS]; // kernel region: the game writes the compressed frames here
int in_free[IN_BUFS], nin_free;
Pending pending[IN_BUFS]; // sent, not decoded yet (the worker takes them in order)
int npending;
uint8_t *out[OUT_BUFS]; // NV12 frames
int out_bytes[OUT_BUFS], out_held[OUT_BUFS];
Ready decoded[OUT_BUFS]; // decoded, not due yet
int ndecoded;
Ready ready[OUT_BUFS]; // handed over: AcquireOutputBufferIndex returns these
int nready;
unsigned owed; // events to signal on the next guest thread (bit per MOVIE_EV_*)
uint64_t t0; // host time of the first frame handed over (pacing)
int64_t pts0; // its presentation time (us)
MovieFit *fit; // picture shrunk inside the frame; NULL = the shapes agree, copy
MovieStats st;
PlatMutex mu;
PlatCond work, idle;
PlatThread *thread;
int busy, quit;
uint64_t gen; // bumped by Flush, Stop, Reset, destruction: a decode in flight is dropped
} Movie;
static MovieGuest G;
static volatile int32_t g_ready;
static PlatMutex g_init = PLAT_MUTEX_INIT;
static Movie *g_movies[MAX_MOVIES];
static volatile int32_t g_count; // live host decoders: movie_tick returns at once when 0
static PlatMutex g_lock = PLAT_MUTEX_INIT; // lock order: g_lock, then Movie.mu
static uint64_t (*g_clock)(void) = kern_now_ns;
static int g_pace = -1;
void movie_set_clock(uint64_t (*clock)(void)) { g_clock = clock ? clock : kern_now_ns; }
void movie_set_pace(int on) { g_pace = on; }
static int pace_on(void) {
if (g_pace < 0) {
const char *e = cfg_get("TOTK_MOVIE_PACE");
g_pace = !(e && !strcmp(e, "0"));
}
return g_pace;
}
static int host_off(void) {
static int v = -1;
if (v < 0) {
const char *e = cfg_get("TOTK_MOVIE_HOST");
v = e && !strcmp(e, "0");
}
return v;
}
static uint64_t need(const char *name) {
uint64_t a = ldr_symbol(name);
if (!a) rt_fatal("movie: guest symbol %s is not loaded", name);
return a;
}
// The guest functions, resolved the first time any entry runs (the modules are loaded and relocated by then).
static void guest_init(void) {
if (g_ready) return;
plat_mutex_lock(&g_init);
if (!g_ready) {
G.signal_event = need("_ZN2nn2os11SignalEventEPNS0_9EventTypeE");
G.buf_set_range = need("_ZN5movie6Buffer8SetRangeEii");
G.buf_set_data = need("_ZN5movie6Buffer18SetDataAndCapacityEPvm");
G.buf_base = need("_ZNK5movie6Buffer4BaseEv");
G.buf_capacity = need("_ZNK5movie6Buffer8CapacityEv");
G.md_find_int32 = need("_ZNK5movie9MediaData9FindInt32EPKcPi");
G.md_find_string = need("_ZNK5movie9MediaData10FindStringEPKcPS2_");
G.md_set_int32 = need("_ZN5movie9MediaData8SetInt32EPKci");
G.md_set_rect = need("_ZN5movie9MediaData7SetRectEPKciiii");
G.md_set_string = need("_ZN5movie9MediaData9SetStringEPKcS2_");
G.vptr = need("_ZTVN5movie7DecoderE") + 0x10;
for (int i = 0; i < MOVIE_ENTRIES; i++) G.orig[i] = need(movie_symbols[i]);
plat_xchg32(&g_ready, 1);
}
plat_mutex_unlock(&g_init);
}
void movie_set_guest(const MovieGuest *g) {
plat_mutex_lock(&g_init);
G = *g;
plat_xchg32(&g_ready, 1);
plat_mutex_unlock(&g_init);
}
#define U(x) ((uint64_t)(uintptr_t)(x))
static void buf_set_range(Cpu *c, uint64_t b, int off, int n) { rt_call(c, G.buf_set_range, 3, b, (uint64_t)off, (uint64_t)n); }
static void buf_set_data(Cpu *c, uint64_t b, uint64_t p, uint64_t n) { rt_call(c, G.buf_set_data, 3, b, p, n); }
static int md_find_int32(Cpu *c, uint64_t md, const char *k, int *v) {
return (uint8_t)rt_call(c, G.md_find_int32, 3, md, U(k), U(v));
}
static void md_set_int32(Cpu *c, uint64_t md, const char *k, int v) { rt_call(c, G.md_set_int32, 3, md, U(k), (uint64_t)(uint32_t)v); }
// The original code of an entry with the caller's arguments; its result stays in x0.
static void pass(Cpu *c, int which) {
uint64_t a[8];
memcpy(a, c->x, sizeof a);
if (!G.orig[which]) rt_fatal("movie: no original code for %s", movie_symbols[which]);
rt_call(c, G.orig[which], 8, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]);
}
static Movie *find(uint64_t guest) {
Movie *m = NULL;
plat_mutex_lock_shared(&g_lock);
for (int i = 0; i < MAX_MOVIES && !m; i++)
if (g_movies[i] && g_movies[i]->guest == guest) m = g_movies[i];
plat_mutex_unlock_shared(&g_lock);
return m;
}
// Events are signalled after the locks are released: the woken game thread calls straight back in.
typedef struct { uint64_t ev[4 * MAX_MOVIES]; int n; } Signals;
static void collect(Movie *m, Signals *s) { // m->mu held
for (int k = 0; k < 4; k++)
if (m->owed & (1u << k) && m->events && s->n < (int)(sizeof s->ev / sizeof *s->ev)) {
uint64_t ev = LD64(m->events + 8 * (uint64_t)k);
if (ev) s->ev[s->n++] = ev;
}
m->owed = 0;
}
static void fire(Cpu *c, const Signals *s) {
for (int i = 0; i < s->n; i++) rt_call(c, G.signal_event, 1, s->ev[i]);
}
static int frame_bytes(const Movie *m) { return m->w * m->h + (m->w / 2) * (m->h / 2) * 2; }
// I420 (libvpx) -> packed NV12: Y rows, then rows of interleaved U, V.
static void to_nv12(const vpx_image_t *img, uint8_t *dst, int w, int h) {
for (int y = 0; y < h; y++)
memcpy(dst + (size_t)y * w, img->planes[VPX_PLANE_Y] + (size_t)y * img->stride[VPX_PLANE_Y], (size_t)w);
uint8_t *uv = dst + (size_t)w * h;
int cw = w / 2;
for (int y = 0; y < h / 2; y++) {
const uint8_t *u = img->planes[VPX_PLANE_U] + (size_t)y * img->stride[VPX_PLANE_U];
const uint8_t *v = img->planes[VPX_PLANE_V] + (size_t)y * img->stride[VPX_PLANE_V];
uint8_t *o = uv + (size_t)y * w;
int x = 0;
for (; x + 16 <= cw; x += 16) {
__m128i a = _mm_loadu_si128((const __m128i *)(u + x)), b = _mm_loadu_si128((const __m128i *)(v + x));
_mm_storeu_si128((__m128i *)(o + 2 * x), _mm_unpacklo_epi8(a, b));
_mm_storeu_si128((__m128i *)(o + 2 * x + 16), _mm_unpackhi_epi8(a, b));
}
for (; x < cw; x++) {
o[2 * x] = u[x];
o[2 * x + 1] = v[x];
}
}
}
// ---- Keeping the movie's shape. The game draws a decoded frame over the whole canvas, so on an output that is not 16:9
// (output_res in totk.ini) the 1920x1080 movies would be stretched. The frame keeps its size; the picture is shrunk
// inside it and the rest is black, so that after the game's stretch the picture has the movie's own shape again: a wider
// output gets a squeezed picture with black at the left and right, a taller one (16:10) black above and below.
// The shrink is a box filter (each output pixel averages the source pixels it covers, weights in 1/4096), separable, with
// the per-column and per-row source spans and weights built once per movie (movie_fit_new), so the frame loop only
// multiplies and adds.
enum { FIT_ONE = 4096, FIT_Y_BLACK = 16, FIT_UV_BLACK = 128 }; // black of limited-range video: Y 16, U = V 128
typedef struct { int n, taps; int *start; uint16_t *wt; } FitAxis; // output i: taps weights from source start[i]
struct MovieFit {
int w, h, x0, y0, pw, ph; // frame, and the picture's corner and size in it (all even)
FitAxis lx, ly, cx, cy; // luma, chroma; columns, rows; n = 0 where the axis is kept
uint8_t *tmp; // horizontally shrunk rows, before the vertical pass
uint32_t *acc; // one row of the vertical sum
};
// Box filter from src_n to dst_n (dst_n < src_n): output i covers [i, i + 1) * src_n / dst_n of the source.
static int axis_build(FitAxis *a, int src_n, int dst_n) {
double s = (double)src_n / dst_n;
a->n = dst_n;
a->taps = (int)s + 2 > src_n ? src_n : (int)s + 2;
a->start = malloc((size_t)dst_n * sizeof *a->start);
a->wt = malloc((size_t)dst_n * (size_t)a->taps * sizeof *a->wt);
if (!a->start || !a->wt) return 0;
for (int i = 0; i < dst_n; i++) {
double lo = i * s, hi = (i + 1) * s;
int st = (int)lo;
if (st > src_n - a->taps) st = src_n - a->taps;
a->start[i] = st;
int sum = 0, big = 0;
uint16_t *w = a->wt + (size_t)i * a->taps;
for (int t = 0; t < a->taps; t++) {
double ov = (st + t + 1 < hi ? st + t + 1 : hi) - (st + t > lo ? st + t : lo);
w[t] = ov > 0 ? (uint16_t)(ov / s * FIT_ONE + 0.5) : 0;
sum += w[t];
if (w[t] > w[big]) big = t;
}
w[big] = (uint16_t)(w[big] + FIT_ONE - sum); // the weights add up to exactly 1
}
return 1;
}
static void axis_free(FitAxis *a) {
free(a->start);
free(a->wt);
}
void movie_fit_free(MovieFit *f) {
if (!f) return;
axis_free(&f->lx), axis_free(&f->ly), axis_free(&f->cx), axis_free(&f->cy);
free(f->tmp);
free(f->acc);
free(f);
}
// The plan for a w x h movie shown on an ow x oh output; NULL when the shapes agree within 1% (the frame is copied).
MovieFit *movie_fit_new(int w, int h, unsigned ow, unsigned oh) {
if (w < 16 || h < 16 || (w | h) & 1 || !ow || !oh) return NULL;
double movie = (double)w / h, out = (double)ow / oh, r = out / movie;
if (r > 0.99 && r < 1.01) return NULL;
MovieFit *f = calloc(1, sizeof *f);
if (!f) {
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
return NULL;
}
f->w = w, f->h = h, f->pw = w, f->ph = h;
if (r > 1) f->pw = (int)(w / r / 2 + 0.5) * 2; // wider output: squeeze sideways
else f->ph = (int)(h * r / 2 + 0.5) * 2; // taller output: squeeze vertically
if (f->pw < 2) f->pw = 2;
if (f->ph < 2) f->ph = 2;
f->x0 = (w - f->pw) / 2 & ~1;
f->y0 = (h - f->ph) / 2 & ~1;
int ok = 1;
if (f->pw < w) ok = axis_build(&f->lx, w, f->pw) && axis_build(&f->cx, w / 2, f->pw / 2);
if (f->ph < h) ok = ok && axis_build(&f->ly, h, f->ph) && axis_build(&f->cy, h / 2, f->ph / 2);
f->tmp = malloc((size_t)f->pw * h);
f->acc = malloc((size_t)f->pw * sizeof *f->acc);
if (!ok || !f->tmp || !f->acc) {
rt_log("movie: out of memory for the picture-shape tables; this movie is stretched");
movie_fit_free(f);
return NULL;
}
return f;
}
// One output row of the horizontal pass: dst gets the shrunk src row (dstep 2 writes every other byte, for U and V).
// The tap count is a constant in the 3 and 4 tap copies (what 2x and below squeezes need), so the compiler unrolls the sum.
static inline __attribute__((always_inline)) void fit_row_taps(const FitAxis *a, const uint8_t *src, uint8_t *dst,
int dstep, int taps) {
for (int i = 0; i < a->n; i++) {
const uint8_t *p = src + a->start[i];
const uint16_t *w = a->wt + (size_t)i * taps;
uint32_t v = FIT_ONE / 2;
for (int t = 0; t < taps; t++) v += w[t] * p[t];
dst[(size_t)i * dstep] = (uint8_t)(v / FIT_ONE);
}
}
static void fit_row_h(const FitAxis *a, const uint8_t *src, uint8_t *dst, int dstep) {
if (a->taps == 3) fit_row_taps(a, src, dst, dstep, 3);
else if (a->taps == 4) fit_row_taps(a, src, dst, dstep, 4);
else fit_row_taps(a, src, dst, dstep, a->taps);
}
// One plane (sw x sh) shrunk into the pw x ph picture whose corner (ox, oy) is in dst; ax/ay NULL = that axis is kept.
static void fit_plane(const MovieFit *f, const uint8_t *src, int ss, int sw, int sh, uint8_t *dst, int ds, int dstep,
int ox, int oy, const FitAxis *ax, const FitAxis *ay, int pw, int ph) {
uint8_t *base = dst + (size_t)oy * ds + (size_t)ox * dstep;
if (!ay) { // sideways only: straight into the frame
for (int y = 0; y < sh; y++) {
const uint8_t *row = src + (size_t)y * ss;
uint8_t *o = base + (size_t)y * ds;
if (ax) fit_row_h(ax, row, o, dstep);
else if (dstep == 1) memcpy(o, row, (size_t)sw);
else for (int x = 0; x < sw; x++) o[(size_t)x * dstep] = row[x];
}
return;
}
const uint8_t *in = src;
int is = ss;
if (ax) {
for (int y = 0; y < sh; y++) fit_row_h(ax, src + (size_t)y * ss, f->tmp + (size_t)y * pw, 1);
in = f->tmp, is = pw;
}
for (int y = 0; y < ph; y++) {
const uint16_t *w = ay->wt + (size_t)y * ay->taps;
const uint8_t *r0 = in + (size_t)ay->start[y] * is;
uint8_t *o = base + (size_t)y * ds;
if (ay->taps == 3) { // the common case, summed in registers
const uint8_t *r1 = r0 + is, *r2 = r1 + is;
for (int x = 0; x < pw; x++)
o[(size_t)x * dstep] = (uint8_t)((FIT_ONE / 2 + w[0] * r0[x] + w[1] * r1[x] + w[2] * r2[x]) / FIT_ONE);
continue;
}
memset(f->acc, 0, (size_t)pw * sizeof *f->acc);
for (int t = 0; t < ay->taps; t++) {
const uint8_t *row = r0 + (size_t)t * is;
uint32_t k = w[t];
for (int x = 0; x < pw; x++) f->acc[x] += k * row[x];
}
for (int x = 0; x < pw; x++) o[(size_t)x * dstep] = (uint8_t)((f->acc[x] + FIT_ONE / 2) / FIT_ONE);
}
}
// I420 planes -> packed NV12 with the picture shrunk inside black bars: w x h of Y, then w x h/2 of interleaved U, V.
void movie_fit_apply(const MovieFit *f, const uint8_t *const plane[3], const int stride[3], uint8_t *dst) {
int w = f->w, h = f->h, cw = w / 2, ch = h / 2;
uint8_t *uv = dst + (size_t)w * h;
// Black only where the picture does not go: whole rows above and below it, the strips left and right of it.
int rx = f->x0 + f->pw, ry = f->y0 + f->ph;
memset(dst, FIT_Y_BLACK, (size_t)f->y0 * w);
memset(dst + (size_t)ry * w, FIT_Y_BLACK, (size_t)(h - ry) * w);
memset(uv, FIT_UV_BLACK, (size_t)(f->y0 / 2) * w);
memset(uv + (size_t)(ry / 2) * w, FIT_UV_BLACK, (size_t)(ch - ry / 2) * w);
for (int y = f->y0; y < ry; y++) {
memset(dst + (size_t)y * w, FIT_Y_BLACK, (size_t)f->x0);
memset(dst + (size_t)y * w + rx, FIT_Y_BLACK, (size_t)(w - rx));
}
for (int y = f->y0 / 2; y < ry / 2; y++) {
memset(uv + (size_t)y * w, FIT_UV_BLACK, (size_t)f->x0);
memset(uv + (size_t)y * w + rx, FIT_UV_BLACK, (size_t)(w - rx));
}
fit_plane(f, plane[0], stride[0], w, h, dst, w, 1, f->x0, f->y0, f->lx.n ? &f->lx : NULL, f->ly.n ? &f->ly : NULL,
f->pw, f->ph);
for (int c = 0; c < 2; c++)
fit_plane(f, plane[1 + c], stride[1 + c], cw, ch, uv + c, w, 2, f->x0 / 2, f->y0 / 2,
f->cx.n ? &f->cx : NULL, f->cy.n ? &f->cy : NULL, f->pw / 2, f->ph / 2);
}
static int free_out(const Movie *m) {
for (int i = 0; i < OUT_BUFS; i++)
if (!m->out_held[i]) return i;
return -1;
}
static int free_outs(const Movie *m) {
int n = 0;
for (int i = 0; i < OUT_BUFS; i++) n += !m->out_held[i];
return n;
}
static int can_take(const Movie *m, const Pending *p) {
int want = (p->size > 0) + ((p->flags & MOVIE_FLAG_EOS) && !m->eos_in);
return free_outs(m) >= want;
}
static void push_decoded(Movie *m, int slot, unsigned flags, int64_t time, int bytes) {
m->out_held[slot] = 1;
m->out_bytes[slot] = bytes;
m->decoded[m->ndecoded++] = (Ready){slot, flags, time};
}
static int vpx_open(Movie *m) {
vpx_codec_dec_cfg_t cfg = {.threads = VPX_THREADS, .w = (unsigned)m->w, .h = (unsigned)m->h};
if (vpx_codec_dec_init(&m->vpx, vpx_codec_vp9_dx(), &cfg, 0) != VPX_CODEC_OK) return 0;
vpx_codec_control(&m->vpx, VP9D_SET_ROW_MT, 1);
m->vpx_live = 1;
return 1;
}
// The worker: takes inputs in order, decodes them outside the lock into a reserved output slot, publishes the frame.
static void worker(void *arg) {
Movie *m = arg;
plat_mutex_lock(&m->mu);
for (;;) {
while (!m->quit && !(m->started && m->vpx_live && m->npending && can_take(m, &m->pending[0])))
plat_cond_wait(&m->work, &m->mu);
if (m->quit) break;
Pending p = m->pending[0];
memmove(m->pending, m->pending + 1, (size_t)--m->npending * sizeof *m->pending);
int slot = -1;
if (p.size > 0) {
slot = free_out(m);
m->out_held[slot] = 1; // reserved: in no queue, so the game cannot release it
}
uint64_t gen = m->gen, dt = 0, fit_ns = 0;
// A plan made for another frame size (a start that found the worker busy) is not used: it would overrun dst.
const MovieFit *fit = m->fit && m->fit->w == m->w && m->fit->h == m->h ? m->fit : NULL;
int got = 0, w = m->w, h = m->h;
uint8_t *dst = slot >= 0 ? m->out[slot] : NULL;
vpx_codec_err_t e = VPX_CODEC_OK;
const char *bad = NULL;
m->busy = 1;
plat_mutex_unlock(&m->mu);
if (p.size > 0) {
uint64_t t0 = kern_now_ns();
e = vpx_codec_decode(&m->vpx, m->in[p.index] + p.offset, (unsigned)p.size, NULL, 0);
if (e == VPX_CODEC_OK) {
vpx_codec_iter_t it = NULL;
const vpx_image_t *img;
while ((img = vpx_codec_get_frame(&m->vpx, &it)) != NULL) {
if (img->fmt != VPX_IMG_FMT_I420 || (int)img->d_w != w || (int)img->d_h != h) bad = "layout";
else if (!got) {
uint64_t t1 = kern_now_ns();
if (fit) movie_fit_apply(fit, (const uint8_t *const *)img->planes, img->stride, dst);
else to_nv12(img, dst, w, h);
fit_ns = kern_now_ns() - t1;
}
got++;
}
}
dt = kern_now_ns() - t0;
}
plat_mutex_lock(&m->mu);
m->busy = 0;
plat_cond_broadcast(&m->idle);
if (m->gen != gen) continue; // flushed, stopped or reset meanwhile: the queues were reset after this
m->in_free[m->nin_free++] = p.index;
m->owed |= 1u << MOVIE_EV_INPUT;
if (p.size > 0) {
m->st.decode_ns += dt;
m->st.fit_ns += fit_ns;
if (dt > m->st.max_ns) m->st.max_ns = dt;
if (bad) rt_fatal("movie: VP9 frame layout differs from the configured %dx%d I420", w, h);
if (e != VPX_CODEC_OK || !got) {
m->out_held[slot] = 0;
if (e != VPX_CODEC_OK) {
m->st.errors++;
m->last_error = ST_ERROR;
m->owed |= 1u << MOVIE_EV_ERROR;
if (m->st.errors <= 4) {
const char *d = vpx_codec_error_detail(&m->vpx);
rt_log("movie: VP9 decode error on a %d-byte frame at %lld us: %s", p.size, (long long)p.time,
d ? d : vpx_codec_err_to_string(e));
}
}
} else {
if (got > 1) rt_log("movie: %d frames from one input at %lld us, the first kept", got, (long long)p.time);
if (!m->format_sent) {
m->format_sent = 1;
m->owed |= 1u << MOVIE_EV_FORMAT;
}
push_decoded(m, slot, 0, p.time, frame_bytes(m));
m->st.frames++;
if (m->st.frames % 600 == 0)
rt_log("movie: %llu frames, decode %.2f ms per frame (max %.2f), of it %.2f ms %s",
(unsigned long long)m->st.frames, m->st.decode_ns / 1e6 / (double)m->st.frames,
m->st.max_ns / 1e6, m->st.fit_ns / 1e6 / (double)m->st.frames,
m->fit ? "fitting the picture" : "converting");
}
}
if (p.flags & MOVIE_FLAG_EOS && !m->eos_in) {
m->eos_in = 1;
push_decoded(m, free_out(m), MOVIE_FLAG_EOS, p.time, 0);
rt_log("movie: end of stream after %llu frames (decode %.2f ms per frame, max %.2f, %llu errors)",
(unsigned long long)m->st.frames, m->st.frames ? m->st.decode_ns / 1e6 / (double)m->st.frames : 0.0,
m->st.max_ns / 1e6, (unsigned long long)m->st.errors);
}
}
plat_mutex_unlock(&m->mu);
return;
}
// With m->mu held: invalidate a decode in flight and wait for the worker to leave it. Returns whether the calling guest
// thread gave its core up; the caller calls kern_core_back after releasing m->mu.
static int quiesce(Movie *m) {
int away = 0;
m->gen++;
while (m->busy) {
if (!away) away = kern_core_away();
plat_cond_wait(&m->idle, &m->mu);
}
return away;
}
static void unlock_back(Movie *m, int away) {
plat_mutex_unlock(&m->mu);
kern_core_back(&away);
}
static void reset_queues(Movie *m) {
m->nin_free = 0;
for (int i = 0; i < IN_BUFS; i++) m->in_free[m->nin_free++] = i;
memset(m->out_held, 0, sizeof m->out_held);
m->npending = m->nready = m->ndecoded = 0;
m->eos_in = 0;
m->t0 = 0;
}
static void release_codec(Movie *m) { // m->mu held, worker quiesced
if (m->vpx_live) {
vpx_codec_destroy(&m->vpx);
m->vpx_live = 0;
}
for (int i = 0; i < IN_BUFS; i++) {
if (m->in[i]) mem_kernel_free(m->in[i], IN_CAP);
m->in[i] = NULL;
}
for (int i = 0; i < OUT_BUFS; i++) {
free(m->out[i]);
m->out[i] = NULL;
}
movie_fit_free(m->fit);
m->fit = NULL;
m->configured = m->started = 0;
reset_queues(m);
}
// Host time a decoded frame is due (ns); end of stream is due as soon as it is first in line.
static uint64_t due_at(const Movie *m, const Ready *d) {
int64_t ahead = d->time - m->pts0;
if (ahead < 0) ahead = 0;
return m->t0 + (uint64_t)ahead * 1000;
}
// One present: drop leading frames more than 1.5 periods late while the next one is due too, then hand over at most one
// due frame (end of stream right after the last frame).
static void release_due(Movie *m, uint64_t now) {
if (!m->ndecoded) return;
if (!pace_on()) {
for (int i = 0; i < m->ndecoded; i++) m->ready[m->nready++] = m->decoded[i];
m->st.handed += m->ndecoded;
m->ndecoded = 0;
m->owed |= 1u << MOVIE_EV_OUTPUT;
return;
}
while (m->ndecoded > 1 && m->t0 && !(m->decoded[0].flags & MOVIE_FLAG_EOS) &&
!(m->decoded[1].flags & MOVIE_FLAG_EOS)) {
uint64_t due = due_at(m, &m->decoded[0]), next = due_at(m, &m->decoded[1]);
if (now < next || now - due <= (next - due) * 3 / 2) break;
m->out_held[m->decoded[0].index] = 0;
m->st.dropped++;
memmove(m->decoded, m->decoded + 1, (size_t)--m->ndecoded * sizeof *m->decoded);
plat_cond_signal(&m->work);
}
Ready d = m->decoded[0];
if (!(d.flags & MOVIE_FLAG_EOS)) {
if (!m->t0) {
m->t0 = now;
m->pts0 = d.time;
}
if (now < due_at(m, &d)) return;
}
memmove(m->decoded, m->decoded + 1, (size_t)--m->ndecoded * sizeof *m->decoded);
m->ready[m->nready++] = d;
m->st.handed++;
m->owed |= 1u << MOVIE_EV_OUTPUT;
}
void movie_tick(Cpu *c) {
if (!g_count) return;
Signals s = {0};
plat_mutex_lock_shared(&g_lock);
for (int i = 0; i < MAX_MOVIES; i++) {
Movie *m = g_movies[i];
if (!m) continue;
plat_mutex_lock(&m->mu);
if (m->started) release_due(m, g_clock());
collect(m, &s);
plat_mutex_unlock(&m->mu);
}
plat_mutex_unlock_shared(&g_lock);
fire(c, &s);
}
int movie_stats(uint64_t obj, MovieStats *out) {
Movie *m = find(obj);
if (!m) return 0;
plat_mutex_lock_shared(&m->mu);
*out = m->st;
plat_mutex_unlock_shared(&m->mu);
return 1;
}
int movie_quiet(uint64_t obj) {
Movie *m = find(obj);
if (!m) return 1;
plat_mutex_lock_shared(&m->mu);
int q = !m->busy && !(m->started && m->vpx_live && m->npending && can_take(m, &m->pending[0]));
plat_mutex_unlock_shared(&m->mu);
return q;
}
// ---- entries
// Decoder(DecoderType type, DecoderOutputFormat format, DecoderMode mode, int priority)
static void ov_ctor(Cpu *c) {
uint64_t obj = arg_x(c, 0);
int type = (int)arg_x(c, 1), format = (int)arg_x(c, 2), mode = (int)arg_x(c, 3);
guest_init();
if (type != MOVIE_TYPE_VP9 || mode != MOVIE_MODE_BUFFER || host_off()) {
rt_log("movie: decoder type %d mode %d format %d left to the library%s", type, mode, format,
host_off() ? " (TOTK_MOVIE_HOST=0)" : " (only VP9 in buffer mode runs on the host)");
pass(c, MOVIE_CTOR);
return;
}
Movie *m = calloc(1, sizeof *m);
if (!m) rt_fatal("movie: decoder allocation");
m->guest = obj;
m->format = format;
plat_mutex_init(&m->mu);
plat_cond_init(&m->work);
plat_cond_init(&m->idle);
m->thread = plat_thread_new(worker, m, "movie");
if (!m->thread) rt_fatal("movie: cannot start the worker thread");
plat_mutex_lock(&g_lock);
int i = 0;
while (i < MAX_MOVIES && g_movies[i] && g_movies[i]->guest != obj) i++;
if (i == MAX_MOVIES) rt_fatal("movie: more than %d live decoders", MAX_MOVIES);
if (g_movies[i]) rt_fatal("movie: decoder object %#llx constructed twice", (unsigned long long)obj);
g_movies[i] = m;
plat_inc32(&g_count);
plat_mutex_unlock(&g_lock);
ST64(obj, G.vptr);
ST64(obj + 8, U(m));
rt_log("movie: VP9 decoder %#llx on the host (libvpx %s), output format %d", (unsigned long long)obj,
vpx_codec_version_str(), format);
}
static void destroy(Cpu *c, uint64_t obj) {
(void)c;
Movie *m = NULL;
plat_mutex_lock(&g_lock);
for (int i = 0; i < MAX_MOVIES; i++)
if (g_movies[i] && g_movies[i]->guest == obj) {
m = g_movies[i];
g_movies[i] = NULL;
plat_dec32(&g_count);
}
plat_mutex_unlock(&g_lock);
if (!m) return;
plat_mutex_lock(&m->mu);
m->quit = 1;
m->gen++;
plat_cond_broadcast(&m->work);
plat_mutex_unlock(&m->mu);
int away = kern_core_away();
plat_thread_join(m->thread);
kern_core_back(&away);
rt_log("movie: decoder %#llx destroyed after %llu frames (%llu dropped to keep time)", (unsigned long long)obj,
(unsigned long long)m->st.frames, (unsigned long long)m->st.dropped);
release_codec(m);
free(m);
ST64(obj, G.vptr); // what the library's destructor leaves (no DecoderImpl)
ST64(obj + 8, 0);
}
static void ov_d2(Cpu *c) {
uint64_t obj = arg_x(c, 0);
if (!find(obj)) { pass(c, MOVIE_D2); return; }
destroy(c, obj);
}
// Deleting destructor: free the host state, then the library's deleting destructor frees the object (impl now 0).
static void ov_d0(Cpu *c) {
uint64_t obj = arg_x(c, 0);
if (find(obj)) destroy(c, obj);
pass(c, MOVIE_D0);
}
// Status Configure(MediaData *format)
static void ov_configure(Cpu *c) {
uint64_t obj = arg_x(c, 0), md = arg_x(c, 1);
Movie *m = find(obj);
if (!m) { pass(c, MOVIE_CONFIGURE); return; }
if (!md) { ret_x(c, ST_NULL); return; }
if (m->format != MOVIE_FORMAT_NV12) {
rt_log("movie: output format %d not implemented on the host (NV12 = 4 only)", m->format);
ret_x(c, ST_UNSUPPORTED);
return;
}
uint64_t mime_p = 0;
int w = 0, h = 0;
rt_call(c, G.md_find_string, 3, md, U("mime"), U(&mime_p));
md_find_int32(c, md, "width", &w);
md_find_int32(c, md, "height", &h);
const char *mime = (const char *)(uintptr_t)mime_p;
if (!mime || strcmp(mime, "video/x-vnd.on2.vp9") || w <= 0 || h <= 0 || w > 8192 || h > 8192 || (w | h) & 1) {
rt_log("movie: configure: mime %s %dx%d not supported", mime ? mime : "(none)", w, h);
ret_x(c, ST_UNSUPPORTED);
return;
}
plat_mutex_lock(&m->mu);
int away = quiesce(m);
release_codec(m);
m->w = w;
m->h = h;
if (!vpx_open(m)) rt_fatal("movie: libvpx VP9 decoder initialisation");
for (int i = 0; i < IN_BUFS; i++) m->in[i] = mem_kernel_alloc(IN_CAP, 0x1000);
for (int i = 0; i < OUT_BUFS; i++)
if (!(m->out[i] = malloc((size_t)frame_bytes(m)))) rt_fatal("movie: output buffers");
reset_queues(m);
m->format_sent = 0;
m->configured = 1;
m->last_error = 0;
unlock_back(m, away);
rt_log("movie: configured %dx%d VP9, %d input buffers of %d KiB, %d output frames of %d bytes", w, h, IN_BUFS,
IN_CAP >> 10, OUT_BUFS, frame_bytes(m));
ret_x(c, ST_OK);
}
// Status RegisterClientEvents(DecoderEvents *events)
static void ov_register_events(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_REGISTER_EVENTS); return; }
plat_mutex_lock(&m->mu);
m->events = arg_x(c, 1);
plat_mutex_unlock(&m->mu);
ret_x(c, ST_OK);
}
static void set_format(Cpu *c, int w, int h, uint64_t md) {
rt_call(c, G.md_set_string, 3, md, U("mime"), U("video/raw"));
md_set_int32(c, md, "width", w);
md_set_int32(c, md, "height", h);
md_set_int32(c, md, "stride", w);
md_set_int32(c, md, "slice-height", h);
md_set_int32(c, md, "color-format", 21); // COLOR_FormatYUV420SemiPlanar
rt_call(c, G.md_set_rect, 6, md, U("crop"), (uint64_t)0, (uint64_t)0, (uint64_t)(uint32_t)(w - 1),
(uint64_t)(uint32_t)(h - 1));
md_set_int32(c, md, "crop-width", w);
md_set_int32(c, md, "crop-height", h);
md_set_int32(c, md, "nv12-y-stride", w);
md_set_int32(c, md, "nv12-uv-stride", w);
md_set_int32(c, md, "nv12-y-offset", 0);
md_set_int32(c, md, "nv12-uv-offset", w * h);
}
// Status GetOutputFormat(MediaData *format)
static void ov_get_output_format(Cpu *c) {
uint64_t md = arg_x(c, 1);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_OUTPUT_FORMAT); return; }
if (!md) { ret_x(c, ST_NULL); return; }
plat_mutex_lock_shared(&m->mu);
int ok = m->configured, w = m->w, h = m->h;
plat_mutex_unlock_shared(&m->mu);
if (!ok) { ret_x(c, ST_ERROR); return; }
set_format(c, w, h, md);
ret_x(c, ST_OK);
}
static void ov_start(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_START); return; }
Signals s = {0};
plat_mutex_lock(&m->mu);
int st = ST_NOT_CONFIGURED;
if (m->configured) {
if (!m->started && !m->busy) { // the worker is idle: it reads m->fit only when it takes an input
movie_fit_free(m->fit);
m->fit = NULL;
unsigned out[2];
res_wanted(out);
if (strcmp(cfg_value("TOTK_MOVIE_ASPECT"), "stretch")) {
m->fit = movie_fit_new(m->w, m->h, out[0], out[1]);
if (m->fit)
rt_log("movie: %dx%d picture kept in shape on the %ux%u output (%dx%d inside the frame)", m->w,
m->h, out[0], out[1], m->fit->pw, m->fit->ph);
}
}
m->started = 1;
m->owed |= 1u << MOVIE_EV_INPUT;
plat_cond_signal(&m->work);
st = ST_OK;
}
collect(m, &s);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
static void ov_stop(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_STOP); return; }
plat_mutex_lock(&m->mu);
int away = quiesce(m);
rt_log("movie: stop after %llu frames (%llu dropped to keep time; decode %.2f ms per frame, max %.2f)",
(unsigned long long)m->st.frames, (unsigned long long)m->st.dropped,
m->st.frames ? m->st.decode_ns / 1e6 / (double)m->st.frames : 0.0, m->st.max_ns / 1e6);
release_codec(m);
unlock_back(m, away);
ret_x(c, ST_OK);
}
static void ov_reset(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_RESET); return; }
plat_mutex_lock(&m->mu);
int away = quiesce(m);
release_codec(m);
m->last_error = 0;
unlock_back(m, away);
ret_x(c, ST_OK);
}
// Drop queued work; the next input starts a new decode (after a seek: a key frame).
static void ov_flush(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_FLUSH); return; }
plat_mutex_lock(&m->mu);
int away = quiesce(m);
if (m->configured) {
vpx_codec_destroy(&m->vpx);
m->vpx_live = 0;
if (!vpx_open(m)) rt_fatal("movie: libvpx VP9 decoder re-initialisation");
reset_queues(m);
}
unlock_back(m, away);
ret_x(c, ST_OK);
}
// Status AcquireInputBufferIndex(int *index, int *remaining)
static void ov_acquire_input(Cpu *c) {
int *index = arg_p(c, 1), *remaining = arg_p(c, 2);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_ACQUIRE_INPUT); return; }
if (!index || !remaining) { ret_x(c, ST_NULL); return; }
Signals s = {0};
plat_mutex_lock(&m->mu);
*index = -1;
*remaining = 0;
int st = ST_NO_INPUT;
if (m->started && m->nin_free > 0) {
*index = m->in_free[0];
memmove(m->in_free, m->in_free + 1, (size_t)--m->nin_free * sizeof *m->in_free);
*remaining = m->nin_free;
st = ST_OK;
}
collect(m, &s);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
// Status GetInputBuffer(int index, Buffer *buffer)
static void ov_get_input(Cpu *c) {
int index = (int)arg_x(c, 1);
uint64_t buf = arg_x(c, 2);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_INPUT); return; }
if (!buf) { ret_x(c, ST_NULL); return; }
plat_mutex_lock_shared(&m->mu);
uint8_t *p = index >= 0 && index < IN_BUFS ? m->in[index] : NULL;
plat_mutex_unlock_shared(&m->mu);
buf_set_range(c, buf, 0, p ? IN_CAP : 0);
buf_set_data(c, buf, U(p), p ? IN_CAP : 0);
ret_x(c, p ? ST_OK : ST_ERROR);
}
// Status SendInputBufferForDecode(int index, int offset, int size, long time, unsigned flags)
static void ov_send_input(Cpu *c) {
int index = (int)arg_x(c, 1), offset = (int)arg_x(c, 2), size = (int)arg_x(c, 3);
int64_t time = (int64_t)arg_x(c, 4);
unsigned flags = (unsigned)arg_x(c, 5);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_SEND_INPUT); return; }
if (index < 0 || index >= IN_BUFS || offset < 0 || size < 0 || (int64_t)offset + size > IN_CAP) {
ret_x(c, ST_ERROR);
return;
}
Signals s = {0};
plat_mutex_lock(&m->mu);
int st = ST_OK;
if (!m->started || m->npending == IN_BUFS) st = ST_ERROR;
else if (m->eos_in) { // after end of stream: hand the buffer back
m->in_free[m->nin_free++] = index;
m->owed |= 1u << MOVIE_EV_INPUT;
} else {
m->pending[m->npending++] = (Pending){index, offset, size, time, flags};
plat_cond_signal(&m->work);
}
collect(m, &s);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
// Status AcquireOutputBufferIndex(int *index, long *time, unsigned *flags, int *remaining)
static void ov_acquire_output(Cpu *c) {
int *index = arg_p(c, 1), *remaining = arg_p(c, 4);
int64_t *time = arg_p(c, 2);
unsigned *flags = arg_p(c, 3);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_ACQUIRE_OUTPUT); return; }
if (!index || !time || !flags || !remaining) { ret_x(c, ST_NULL); return; }
Signals s = {0};
plat_mutex_lock(&m->mu); // paced frames become ready only at presents (movie_tick)
*index = -1;
*time = 0;
*flags = 0;
*remaining = 0;
int st = ST_NO_OUTPUT;
if (m->nready > 0) {
Ready r = m->ready[0];
memmove(m->ready, m->ready + 1, (size_t)--m->nready * sizeof *m->ready);
*index = r.index;
*time = r.time;
*flags = r.flags;
*remaining = m->nready;
st = ST_OK;
}
collect(m, &s);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
static int get_output(Cpu *c, Movie *m, int index, uint64_t buf) {
buf_set_range(c, buf, 0, 0);
uint8_t *dst = (uint8_t *)(uintptr_t)rt_call(c, G.buf_base, 1, buf);
uint64_t cap = rt_call(c, G.buf_capacity, 1, buf);
int st = ST_ERROR, bytes = 0;
plat_mutex_lock_shared(&m->mu);
if (index >= 0 && index < OUT_BUFS && m->out_held[index]) {
bytes = m->out_bytes[index];
if (!bytes) st = ST_OK;
else if (dst && cap >= (uint64_t)bytes) {
memcpy(dst, m->out[index], (size_t)bytes);
st = ST_OK;
} else {
rt_log("movie: output buffer of %llu bytes for a %d-byte frame", (unsigned long long)cap, bytes);
bytes = 0;
}
}
plat_mutex_unlock_shared(&m->mu);
if (st == ST_OK && bytes) buf_set_range(c, buf, 0, bytes);
return st;
}
// Status GetOutputBuffer(int index, Buffer *buffer)
static void ov_get_output(Cpu *c) {
int index = (int)arg_x(c, 1);
uint64_t buf = arg_x(c, 2);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_OUTPUT); return; }
ret_x(c, buf ? (uint32_t)get_output(c, m, index, buf) : ST_NULL);
}
// Status GetOutputBufferSize(int *size, int *count)
static void ov_get_output_size(Cpu *c) {
int *size = arg_p(c, 1), *count = arg_p(c, 2);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_OUTPUT_SIZE); return; }
if (!size || !count) { ret_x(c, ST_NULL); return; }
plat_mutex_lock_shared(&m->mu);
*count = 1;
*size = m->configured ? frame_bytes(m) : 0;
plat_mutex_unlock_shared(&m->mu);
ret_x(c, ST_OK);
}
// Status ReleaseOutputBufferIndex(int index)
static void ov_release_output(Cpu *c) {
int index = (int)arg_x(c, 1);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_RELEASE_OUTPUT); return; }
Signals s = {0};
plat_mutex_lock(&m->mu);
int st = ST_ERROR;
if (index >= 0 && index < OUT_BUFS && m->out_held[index]) {
int queued = 0;
for (int i = 0; i < m->nready; i++) queued |= m->ready[i].index == index;
for (int i = 0; i < m->ndecoded; i++) queued |= m->decoded[i].index == index;
if (!queued) {
m->out_held[index] = 0;
plat_cond_signal(&m->work);
st = ST_OK;
}
}
collect(m, &s);
plat_mutex_unlock(&m->mu);
fire(c, &s);
ret_x(c, (uint32_t)st);
}
static void ov_get_last_error(Cpu *c) {
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_LAST_ERROR); return; }
plat_mutex_lock_shared(&m->mu);
int e = m->last_error;
plat_mutex_unlock_shared(&m->mu);
ret_x(c, (uint32_t)e);
}
static int output_format(Cpu *c, Movie *m, int index, uint64_t md) {
plat_mutex_lock_shared(&m->mu);
int ok = index >= 0 && index < OUT_BUFS && m->out_held[index], w = m->w, h = m->h;
plat_mutex_unlock_shared(&m->mu);
if (!ok) return ST_ERROR;
set_format(c, w, h, md);
return ST_OK;
}
// Status GetOutputBufferFormat(int index, MediaData *format)
static void ov_get_output_buffer_format(Cpu *c) {
int index = (int)arg_x(c, 1);
uint64_t md = arg_x(c, 2);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_OUTPUT_BUFFER_FORMAT); return; }
ret_x(c, md ? (uint32_t)output_format(c, m, index, md) : ST_NULL);
}
// Status GetOutputBuffer(int index, Buffer *buffer, MediaData *format)
static void ov_get_output_md(Cpu *c) {
int index = (int)arg_x(c, 1);
uint64_t buf = arg_x(c, 2), md = arg_x(c, 3);
Movie *m = find(arg_x(c, 0));
if (!m) { pass(c, MOVIE_GET_OUTPUT_MD); return; }
if (!buf && !md) { ret_x(c, ST_NULL); return; }
int st = md ? output_format(c, m, index, md) : ST_OK;
if (st == ST_OK && buf) st = get_output(c, m, index, buf);
ret_x(c, (uint32_t)st);
}
HOST_OVERRIDE("_ZN5movie7DecoderC1ENS_11DecoderTypeENS_19DecoderOutputFormatENS_11DecoderModeEi", ov_ctor);
HOST_OVERRIDE("_ZN5movie7DecoderD2Ev", ov_d2);
// 1.4.3's vtable and a PLT call name the complete-object destructor; without virtual bases it is D2.
HOST_OVERRIDE("_ZN5movie7DecoderD1Ev", ov_d2);
HOST_OVERRIDE("_ZN5movie7DecoderD0Ev", ov_d0);
HOST_OVERRIDE("_ZN5movie7Decoder9ConfigureEPNS_9MediaDataE", ov_configure);
HOST_OVERRIDE("_ZN5movie7Decoder20RegisterClientEventsEPNS_13DecoderEventsE", ov_register_events);
HOST_OVERRIDE("_ZN5movie7Decoder15GetOutputFormatEPNS_9MediaDataE", ov_get_output_format);
HOST_OVERRIDE("_ZN5movie7Decoder5StartEv", ov_start);
HOST_OVERRIDE("_ZN5movie7Decoder4StopEv", ov_stop);
HOST_OVERRIDE("_ZN5movie7Decoder5ResetEv", ov_reset);
HOST_OVERRIDE("_ZN5movie7Decoder5FlushEv", ov_flush);
HOST_OVERRIDE("_ZN5movie7Decoder23AcquireInputBufferIndexEPiS1_", ov_acquire_input);
HOST_OVERRIDE("_ZN5movie7Decoder14GetInputBufferEiPNS_6BufferE", ov_get_input);
HOST_OVERRIDE("_ZN5movie7Decoder24SendInputBufferForDecodeEiiilj", ov_send_input);
HOST_OVERRIDE("_ZN5movie7Decoder24AcquireOutputBufferIndexEPiPlPjS1_", ov_acquire_output);
HOST_OVERRIDE("_ZN5movie7Decoder15GetOutputBufferEiPNS_6BufferE", ov_get_output);
HOST_OVERRIDE("_ZN5movie7Decoder19GetOutputBufferSizeEPiS1_", ov_get_output_size);
HOST_OVERRIDE("_ZN5movie7Decoder24ReleaseOutputBufferIndexEi", ov_release_output);
HOST_OVERRIDE("_ZN5movie7Decoder12GetLastErrorEv", ov_get_last_error);
HOST_OVERRIDE("_ZN5movie7Decoder15GetOutputBufferEiPNS_6BufferEPNS_9MediaDataE", ov_get_output_md);
HOST_OVERRIDE("_ZN5movie7Decoder21GetOutputBufferFormatEiPNS_9MediaDataE", ov_get_output_buffer_format);