mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
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>
1068 lines
45 KiB
C
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);
|