aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--3p/ms/getopt.c51
-rw-r--r--3p/ms/getopt.h36
-rw-r--r--Makefile6
-rw-r--r--README9
-rw-r--r--api.c32
-rw-r--r--api.h96
-rw-r--r--main.c81
-rw-r--r--os.h2
-rw-r--r--render.c199
-rw-r--r--render.h37
10 files changed, 509 insertions, 40 deletions
diff --git a/3p/ms/getopt.c b/3p/ms/getopt.c
new file mode 100644
index 0000000..307baf2
--- /dev/null
+++ b/3p/ms/getopt.c
@@ -0,0 +1,51 @@
+/* *****************************************************************
+*
+* Copyright 2016 Microsoft
+*
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+******************************************************************/
+
+#include "getopt.h"
+#include <windows.h>
+
+char* optarg = NULL;
+int optind = 1;
+
+int getopt(int argc, char *const argv[], const char *optstring)
+{
+ if ((optind >= argc) || (argv[optind][0] != '-') || (argv[optind][0] == 0))
+ {
+ return -1;
+ }
+
+ int opt = argv[optind][1];
+ const char *p = strchr(optstring, opt);
+
+ if (p == NULL)
+ {
+ return '?';
+ }
+ if (p[1] == ':')
+ {
+ optind++;
+ if (optind >= argc)
+ {
+ return '?';
+ }
+ optarg = argv[optind];
+ optind++;
+ }
+ return opt;
+}
diff --git a/3p/ms/getopt.h b/3p/ms/getopt.h
new file mode 100644
index 0000000..33de8ad
--- /dev/null
+++ b/3p/ms/getopt.h
@@ -0,0 +1,36 @@
+/* *****************************************************************
+*
+* Copyright 2016 Microsoft
+*
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*
+******************************************************************/
+
+#ifndef GETOPT_H__
+#define GETOPT_H__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern char *optarg;
+extern int optind;
+
+int getopt(int argc, char *const argv[], const char *optstring);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/Makefile b/Makefile
index 5001b4b..94bf072 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,10 @@
CC = clang
-OBJS = main.o api.o 3p/sst/x86.o hook.o
WARNINGS=-Wall -Wpedantic -Wextra -Wno-gnu-zero-variadic-macro-arguments \
-D_CRT_SECURE_NO_WARNINGS
-CFLAGS:=$(CFLAGS) -m32 -flto -I3p -std=c23
+override CFLAGS+=-m32 -flto -I3p -std=c23
+override LDFLAGS+=-lmf -lmfplat -lmfuuid -lmfreadwrite -lstrmiids
+
+OBJS = main.o api.o 3p/sst/x86.o 3p/ms/getopt.o hook.o render.o
all: rt.exe
rt.exe: $(OBJS)
diff --git a/README b/README
index 0189cfb..168b6a1 100644
--- a/README
+++ b/README
@@ -4,4 +4,11 @@ rendertools
orange box engine demo renderer
usage:
- rt -w <width> -h <height> -g <game> -r <fps> path/to/demo.dem
+ rt -w <width> -h <height> -g <game> -r <fps> [-1] [-q <quality>] [-b <bitrate>]
+ path/to/output.mp4 path/to/demo1.dem [path/to/demo2.dem...]
+
+-1 flag combines multiple renders into 1 file
+
+bitrate is in kbps, quality 1-100
+cannot specify quality and bitrate at the same time
+demo path is relative to the gamedir (e.g. hl2)
diff --git a/api.c b/api.c
index 5dba38a..0562bba 100644
--- a/api.c
+++ b/api.c
@@ -74,7 +74,9 @@ bool api_init(void) {
// find videomode
// CEngineAPI::SetEngineWindow calls videomode->SetGameWindow after
// detaching the current window with another vcall. Look for the second one.
- // This, like the demoplayer, is a pointer. Mind the double dereference.
+ // This, like the demoplayer, is a pointer. We need to keep the double
+ // pointer because when we get this, it is null. It is set by the engine
+ // later.
instr = (const u8 *)engineapi->vt->set_engine_window;
int mov_ecx_counter = 0;
for (const u8 *p = instr; p - instr < 64;) {
@@ -89,6 +91,34 @@ bool api_init(void) {
p += l;
}
debug("videomode = %p", (void *)videomode);
+
+ // find cl_movieinfo
+ {
+ // step 1: find CL_IsRecordingMovie() in
+ // CEngineTools::StartMovieRecording
+ instr = (const u8 *)enginetools->vt->start_movie_recording;
+ void *cl_isrecordingmovie = NULL;
+ for (const u8 *p = instr; p - instr < 64;) {
+ // this is the first call in the function
+ if (*p == X86_CALL) {
+ cl_isrecordingmovie = (void *)(p + 5 + *(i32 *)(p + 1));
+ }
+ int l = x86_len(p);
+ if (l == -1)
+ bail("invalid instruction looking for CL_IsRecordingMovie");
+ p += l;
+ }
+ if (!cl_isrecordingmovie)
+ bail("couldn't find cbuf_addtext CL_IsRecordingMovie");
+
+ // step 2: get the pointer to cl_movieinfo
+ // should be in the first instruction
+ instr = (const u8 *)cl_isrecordingmovie;
+ if (instr[0] != X86_ALUMI8 || instr[1] != X86_MODRM(0, 7, 5))
+ bail("couldn't get movieinfo");
+ movieinfo = *(struct movieinfo **)(instr + 2);
+ debug("movieinfo = %p", (void *)movieinfo);
+ }
return true;
}
diff --git a/api.h b/api.h
index ea9bef3..8f42eaf 100644
--- a/api.h
+++ b/api.h
@@ -23,20 +23,21 @@
#define VENGINE_TOOL_INTERFACE_VERSION "VENGINETOOL003"
#include "intdef.h"
+#include "os.h"
typedef void * (*createinterface_func)(const char *name, int *ret);
struct engserver {
struct {
usize _pad[36];
- void (*__thiscall server_command)(struct engserver *this, const char *str);
+ void (*VIRTUAL server_command)(struct engserver *this, const char *str);
} *vt;
};
struct engclient {
struct {
usize _pad[75];
- void (*__thiscall is_playing_demo)(struct engclient *this);
+ void (*VIRTUAL is_playing_demo)(struct engclient *this);
} *vt;
};
@@ -49,50 +50,85 @@ struct engineapi {
struct enginetools {
struct {
- usize _pad[64];
- void *is_recording_movie;
+ usize _pad[65];
+ void *start_movie_recording;
} *vt;
};
struct movieinfo {
- char name[256];
- int curframe;
- int type;
- int jpeg_quality;
+ char name[256];
+ int curframe;
+ int type;
+ int jpeg_quality;
+};
+
+// Almost no chance I need all of these, but why not.
+enum image_format {
+ IMAGE_FORMAT_UNKNOWN = -1,
+ IMAGE_FORMAT_RGBA8888 = 0,
+ IMAGE_FORMAT_ABGR8888,
+ IMAGE_FORMAT_RGB888,
+ IMAGE_FORMAT_BGR888,
+ IMAGE_FORMAT_RGB565,
+ IMAGE_FORMAT_I8,
+ IMAGE_FORMAT_IA88,
+ IMAGE_FORMAT_P8,
+ IMAGE_FORMAT_A8,
+ IMAGE_FORMAT_RGB888_BLUESCREEN,
+ IMAGE_FORMAT_BGR888_BLUESCREEN,
+ IMAGE_FORMAT_ARGB8888,
+ IMAGE_FORMAT_BGRA8888,
+ IMAGE_FORMAT_DXT1,
+ IMAGE_FORMAT_DXT3,
+ IMAGE_FORMAT_DXT5,
+ IMAGE_FORMAT_BGRX8888,
+ IMAGE_FORMAT_BGR565,
+ IMAGE_FORMAT_BGRX5551,
+ IMAGE_FORMAT_BGRA4444,
+ IMAGE_FORMAT_DXT1_ONEBITALPHA,
+ IMAGE_FORMAT_BGRA5551,
+ IMAGE_FORMAT_UV88,
+ IMAGE_FORMAT_UVWQ8888,
+ IMAGE_FORMAT_RGBA16161616F,
+ IMAGE_FORMAT_RGBA16161616,
+ IMAGE_FORMAT_UVLX8888,
+ IMAGE_FORMAT_R32F, // Single-channel 32-bit floating point
+ IMAGE_FORMAT_RGB323232F,
+ IMAGE_FORMAT_RGBA32323232F,
+ // vendor crap
+ IMAGE_FORMAT_NV_DST16,
+ IMAGE_FORMAT_NV_DST24,
+ IMAGE_FORMAT_NV_INTZ,
+ IMAGE_FORMAT_NV_RAWZ,
+ IMAGE_FORMAT_ATI_DST16,
+ IMAGE_FORMAT_ATI_DST24,
+ IMAGE_FORMAT_NV_NULL,
+ IMAGE_FORMAT_ATI2N,
+ IMAGE_FORMAT_ATI1N,
+ NUM_IMAGE_FORMATS
};
struct videomode {
struct {
usize _pad[22];
- void (*__thiscall write_movie_frame)(struct videomode *this,
+ void (*VIRTUAL write_movie_frame)(struct videomode *this,
struct movieinfo *info);
+ usize _pad2[4];
+ void (*VIRTUAL read_screen_pixels)(struct videomode *this,
+ int x, int y, int w, int h, void *buf, enum image_format fmt);
} *vt;
};
struct demoplayer {
struct {
- void *_destructor;
- void * (*__thiscall get_demo_file)(struct demoplayer *this);
- int (*__thiscall get_playback_tick)(struct demoplayer *this);
- int (*__thiscall get_total_ticks)(struct demoplayer *this);
- void *_whatisthisihavenoidea; // TODO
- bool (*__thiscall start_playback)(struct demoplayer *this,
+ usize _pad[5];
+ bool (*VIRTUAL start_playback)(struct demoplayer *this,
const char *filename, bool as_time_demo);
- bool (*__thiscall is_playing_back)(struct demoplayer *this);
- bool (*__thiscall is_playback_paused)(struct demoplayer *this);
- bool (*__thiscall is_playing_time_demo)(struct demoplayer *this);
- bool (*__thiscall is_skipping)(struct demoplayer *this);
- bool (*__thiscall can_skip_backwards)(struct demoplayer *this);
- void (*__thiscall set_playback_time_scale)(struct demoplayer *this,
- float timescale);
- float (*__thiscall get_playback_time_scale)(struct demoplayer *this);
- void (*__thiscall pause_playback)(struct demoplayer *this,
- float seconds);
- void (*__thiscall skip_to_tick)(struct demoplayer *this, int tick,
- bool relative, bool pause);
- void (*__thiscall resume_playback)(struct demoplayer *this);
- void (*__thiscall stop_playback)(struct demoplayer *this);
- void (*__thiscall interpolate_viewpoint)(struct demoplayer *this);
+ usize _pad2[11];
+ void (*VIRTUAL stop_playback)(struct demoplayer *this);
+ // TODO: Disable interp when in a portal bubble. This will be very
+ // difficult!
+ void (*VIRTUAL interpolate_viewpoint)(struct demoplayer *this);
} *vt;
};
diff --git a/main.c b/main.c
index 8f487a1..a8b5725 100644
--- a/main.c
+++ b/main.c
@@ -17,21 +17,42 @@
#include "api.h"
#include "hook.h"
#include "log.h"
+#include "render.h"
#include "os.h"
+#include "ms/getopt.h"
+
#include <stddef.h>
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
+struct {
+ int width;
+ int height;
+ char *game;
+ int fps;
+ int quality;
+ int bitrate;
+ char *out;
+ char **demo;
+ bool combine;
+} args = {0};
+
void (*orig_cbuf_addtext)(char *);
void hook_cbuf_addtext(char *str) {
orig_cbuf_addtext(str);
// this is the last thing that happens when the game is opened
if (!strcmp(str, "exec modsettings.cfg mod\n")) {
+ bool use_bitrate = !!args.bitrate;
+ if (!render_init(args.width, args.height, args.fps, use_bitrate,
+ use_bitrate ? args.bitrate : args.quality, args.out))
+ die("couldn't init render");
+ // play the demo
+ demoplayer->vt->start_playback(demoplayer, args.demo[0], false);
}
}
-char *cmdline;
+char cmdline[128];
char WINAPI *hook_GetCommandLineA(void) {
return cmdline;
}
@@ -59,19 +80,67 @@ void WINAPI *hook_LoadLibraryExA(const char *filename, void *hfile, int flags) {
typedef int (*LauncherMain_t)(void *instance, void *prev_inst, char *cmdline,
int cmd_show);
-int main(void/* int argc, char **argv */) {
+int main(int argc, char **argv) {
SetDllDirectoryA("bin/");
+ int c;
+ char *strend;
+ while ((c = getopt(argc, argv, "w:h:g:r:q:b:")) != -1) {
+ switch (c) {
+ case 'w':
+ args.width = strtol(optarg, &strend, 10);
+ if (strend == optarg) die("width must be a number!");
+ break;
+ case 'h':
+ args.height = strtol(optarg, &strend, 10);
+ if (strend == optarg) die("height must be a number");
+ break;
+ case 'r':
+ args.fps = strtol(optarg, &strend, 10);
+ if (strend == optarg) die("must pass a number to -w!");
+ break;
+ case 'q':
+ args.quality = strtol(optarg, &strend, 10);
+ if (strend == optarg) die("must pass a number to -w!");
+ break;
+ case 'b':
+ args.bitrate = strtol(optarg, &strend, 10);
+ if (strend == optarg) die("must pass a number to -w!");
+ break;
+ case 'g':
+ args.game = optarg;
+ case '1':
+ args.combine = true;
+ case '?':
+ break;
+ }
+ }
+
+ if (!args.width) args.width = 1280;
+ if (!args.height) args.height = 720;
+ if (!args.game) args.game = "hl2";
+ if (!args.fps) args.fps = 30;
+ if (!args.quality) args.quality = 75;
- // TODO: make this changeable by the user
- cmdline = "hl2.exe -console -w 1280 -h 720 -window -high -dxlevel 95";
+ if (argc - optind < 2) {
+ printf(
+ "usage:\n"
+ " rt -w <width> -h <height> -g <game> -r <fps> [-q <quality>] [-b <bitrate>]\n"
+ " path/to/video.mp4 path/to/demo1.dem...\n");
+ exit(1);
+ }
+ args.out = argv[optind++];
+ args.demo = argv + optind;
+
+ sprintf(cmdline, "hl2.exe -game %s -w %d -h %d -window -console",
+ args.game, args.width, args.height);
+
+ info("cmdline = %s", cmdline);
hook_init();
orig_LoadLibraryExA = (typeof(orig_LoadLibraryExA))hook_dllapi("kernel32",
"LoadLibraryExA", (void *)hook_LoadLibraryExA);
hook_dllapi("kernel32", "GetCommandLineA", (void *)hook_GetCommandLineA);
- info("GetCommandLineA() = %s", GetCommandLineA());
-
void *launcher_dll = os_dlopen("launcher");
LauncherMain_t launcher_main =
(LauncherMain_t)os_dlsym(launcher_dll, "LauncherMain");
diff --git a/os.h b/os.h
index 36a7757..0eff7ce 100644
--- a/os.h
+++ b/os.h
@@ -33,6 +33,8 @@ inline bool os_mprot(void *mem, int len, int mode) {
return !!VirtualProtect(mem, len, mode, &old);
}
+#define VIRTUAL __thiscall
+
#endif
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/render.c b/render.c
new file mode 100644
index 0000000..495a918
--- /dev/null
+++ b/render.c
@@ -0,0 +1,199 @@
+/*
+ * Copyright © 2024 Matthew Wozniak <me@woz.blue>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+ * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "api.h"
+#include "log.h"
+#include "render.h"
+#include "os.h"
+
+#include <codecapi.h>
+#include <mfapi.h>
+#include <mfobjects.h>
+#include <mfreadwrite.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strmif.h>
+#include <winerror.h>
+
+static struct renderctx ctx = { 0 };
+static bool is_rendering = false;
+
+#define HR(expr) { \
+ HRESULT hr = expr; \
+ if (!SUCCEEDED(hr)) \
+ bail(#expr " failed!!! (code %lu)", hr); \
+}
+
+bool do_frame(struct videomode *this, struct movieinfo *info) {
+ is_rendering = true;
+ IMFSample *sample = NULL;
+ IMFMediaBuffer *imf_buffer = NULL;
+ int szpx = 4;
+ u32 *data = NULL;
+
+ HR(MFCreateMemoryBuffer(ctx.width * ctx.height * szpx, &imf_buffer));
+ HR(imf_buffer->lpVtbl->Lock(imf_buffer, (u8 **)&data, NULL, NULL));
+ // THIS IS SLOW!!
+ u32 *raw = malloc(ctx.width * ctx.height * szpx);
+ this->vt->read_screen_pixels(this, 0, 0, ctx.width, ctx.height, raw, IMAGE_FORMAT_BGRA8888);
+ for (int y = ctx.height - 1; y > 0; y--) {
+ memcpy(data + ((ctx.height - y) * ctx.width), raw + y * ctx.width,
+ szpx * ctx.width);
+ /* for (int x = 0; x < ctx.width; x++) {
+ data[(ctx.height - y) * ctx.width + x] = raw[y * ctx.width + x];
+ } */
+ }
+ free(raw);
+ HR(imf_buffer->lpVtbl->Unlock(imf_buffer));
+ HR(imf_buffer->lpVtbl->SetCurrentLength(imf_buffer, ctx.width * ctx.height * szpx));
+
+ HR(MFCreateSample(&sample));
+ HR(sample->lpVtbl->AddBuffer(sample, imf_buffer));
+ u64 sample_duration = (u64)((1.0 / (double)ctx.fps) * 10000000.0);
+ HR(sample->lpVtbl->SetSampleDuration(sample, sample_duration));
+ u64 sample_time = sample_duration * info->curframe;
+ HR(sample->lpVtbl->SetSampleTime(sample, sample_time));
+ HR(ctx.sink_writer->lpVtbl->WriteSample(ctx.sink_writer, ctx.stream_index, sample));
+
+ sample->lpVtbl->Release(sample);
+ imf_buffer->lpVtbl->Release(imf_buffer);
+ return true;
+}
+
+typeof((*videomode)->vt->write_movie_frame) orig_write_movie_frame;
+void VIRTUAL hook_write_movie_frame(struct videomode *this,
+ struct movieinfo *info) {
+ if (!do_frame(this, info))
+ die("oopsie!");
+}
+
+
+bool do_stop() {
+ HR(ctx.sink_writer->lpVtbl->Flush(ctx.sink_writer, ctx.stream_index));
+ HR(ctx.sink_writer->lpVtbl->Finalize(ctx.sink_writer));
+ HR(MFShutdown());
+ return true;
+}
+
+static void (VIRTUAL *orig_stop_playback)(struct demoplayer *);
+void VIRTUAL hook_stop_playback(struct demoplayer *this) {
+ orig_stop_playback(this);
+
+ if (!is_rendering) return;
+ is_rendering = false;
+ if (!do_stop())
+ die("oopsie!");
+ // TODO: IPC mode so we don't have to restart the game for every demo
+ exit(0);
+}
+
+bool render_init(int width, int height, int framerate, bool use_bitrate,
+ int quality, const char *output_file) {
+ bool r =
+ os_mprot((*videomode)->vt, 28 * sizeof(void *), PAGE_EXECUTE_READWRITE);
+ if (!r) bail("couldn't mprotect videomode vtable");
+ orig_write_movie_frame = (*videomode)->vt->write_movie_frame;
+ (*videomode)->vt->write_movie_frame = hook_write_movie_frame;
+
+ r = os_mprot(demoplayer->vt, 18 * sizeof(void *), PAGE_EXECUTE_READWRITE);
+ if (!r) bail("couldn't mprotect demoplayer vtable");
+ orig_stop_playback = demoplayer->vt->stop_playback;
+ demoplayer->vt->stop_playback = hook_stop_playback;
+
+ // make the game thing we are recording a movie (we are!)
+ memcpy(movieinfo->name, "a", 2);
+ movieinfo->type = 0;
+
+ char framerate_cmd[32] = { 0 };
+ sprintf(framerate_cmd, "host_framerate %d;", framerate);
+ cbuf_addtext(framerate_cmd);
+
+ ctx.fps = framerate;
+ ctx.width = width;
+ ctx.height = height;
+ ctx.bit_rate = use_bitrate ? quality : 99999999;
+
+ MFStartup(MF_VERSION, 0);
+
+ // init sinkwriter
+ {
+ IMFMediaType *mt_out = NULL;
+ IMFMediaType *mt_in = NULL;
+
+ u16 path[MAX_PATH] = {0};
+ info("strlen = %d", strlen(output_file));
+ mbstowcs(path, output_file, strlen(output_file));
+
+ info("rendering to %S", path);
+
+ HR(MFCreateSinkWriterFromURL(path, NULL, NULL, &ctx.sink_writer));
+ HR(MFCreateMediaType(&mt_out));
+ HR(mt_out->lpVtbl->SetGUID(mt_out, &MF_MT_MAJOR_TYPE, &MFMediaType_Video));
+ HR(mt_out->lpVtbl->SetGUID(mt_out, &MF_MT_SUBTYPE, &MFVideoFormat_H264));
+ HR(mt_out->lpVtbl->SetUINT32(mt_out, &MF_MT_AVG_BITRATE, ctx.bit_rate));
+ HR(mt_out->lpVtbl->SetUINT32(mt_out, &MF_MT_MPEG2_PROFILE , eAVEncH264VProfile_Main));
+ HR(mt_out->lpVtbl->SetUINT32(mt_out, &MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive));
+ HR(mt_out->lpVtbl->SetUINT64(mt_out, &MF_MT_FRAME_SIZE, ((u64)ctx.width << 32) | ctx.height));
+ HR(mt_out->lpVtbl->SetUINT64(mt_out, &MF_MT_FRAME_RATE, ((u64)ctx.fps << 32) | 1));
+ HR(mt_out->lpVtbl->SetUINT64(mt_out, &MF_MT_PIXEL_ASPECT_RATIO, (((u64)1 << 32) | 1)));
+ HR(ctx.sink_writer->lpVtbl->AddStream(ctx.sink_writer, mt_out, &ctx.stream_index));
+
+ HR(MFCreateMediaType(&mt_in));
+ HR(mt_in->lpVtbl->SetGUID(mt_in, &MF_MT_MAJOR_TYPE, &MFMediaType_Video));
+ HR(mt_in->lpVtbl->SetGUID(mt_in, &MF_MT_SUBTYPE, &MFVideoFormat_ARGB32));
+ HR(mt_in->lpVtbl->SetUINT32(mt_in, &MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive));
+ HR(mt_in->lpVtbl->SetUINT64(mt_in, &MF_MT_FRAME_SIZE, ((u64)ctx.width << 32) | ctx.height));
+ HR(mt_in->lpVtbl->SetUINT64(mt_in, &MF_MT_FRAME_RATE, ((u64)ctx.fps << 32) | 1));
+ HR(mt_in->lpVtbl->SetUINT64(mt_in, &MF_MT_PIXEL_ASPECT_RATIO, (((u64)1 << 32) | 1)));
+ HR(ctx.sink_writer->lpVtbl->SetInputMediaType(ctx.sink_writer, ctx.stream_index, mt_in, NULL));
+
+ HR(ctx.sink_writer->lpVtbl->BeginWriting(ctx.sink_writer));
+ HR(ctx.sink_writer->lpVtbl->AddRef(ctx.sink_writer));
+ }
+
+ // video encoding parameters
+ {
+ ICodecAPI *codec;
+ HR(ctx.sink_writer->lpVtbl->GetServiceForStream(ctx.sink_writer, 0, &GUID_NULL, &IID_ICodecAPI, (void*)&codec));
+
+ // use a quality VBR
+ int ratecontrol = use_bitrate ? eAVEncCommonRateControlMode_UnconstrainedVBR : eAVEncCommonRateControlMode_Quality;
+ VARIANT _ratecontrol = { .vt = VT_UI4, .ulVal = ratecontrol };
+ HR(codec->lpVtbl->SetValue(codec, &CODECAPI_AVEncCommonRateControlMode, &_ratecontrol));
+
+ // set the quality
+ VARIANT _quality = { .vt = VT_UI4, .ulVal = quality };
+ HR(codec->lpVtbl->SetValue(codec, &CODECAPI_AVEncCommonQuality, &_quality));
+
+ // even at 100 it is still very quick, really no reason to change it
+ VARIANT qvs = { .vt = VT_UI4, .ulVal = 100 };
+ HR(codec->lpVtbl->SetValue(codec, &CODECAPI_AVEncCommonQualityVsSpeed, &qvs));
+
+ // set the gop size to 2 seconds
+ VARIANT gop = { .vt = VT_UI4, .ulVal = 2 * ctx.fps };
+ HR(codec->lpVtbl->SetValue(codec, &CODECAPI_AVEncMPVGOPSize, &gop));
+
+ // enable 2 b-frames
+ VARIANT bframes = { .vt = VT_UI4, .ulVal = 2 };
+ HR(codec->lpVtbl->SetValue(codec, &CODECAPI_AVEncMPVDefaultBPictureCount, &bframes));
+
+ codec->lpVtbl->Release(codec);
+ }
+
+ return true;
+}
+
+// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/render.h b/render.h
new file mode 100644
index 0000000..bec043b
--- /dev/null
+++ b/render.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2024 Matthew Wozniak <me@woz.blue>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+ * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ * PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "intdef.h"
+
+#include <mfapi.h>
+#include <mfidl.h>
+#include <mfreadwrite.h>
+
+bool render_init(int width, int height, int framerate, bool use_bitrate,
+ int quality, const char *output_file);
+
+struct renderctx {
+ int width;
+ int height;
+ int fps;
+ int bit_rate;
+ GUID enc_format;
+ GUID in_fmt;
+ IMFSinkWriter *sink_writer;
+ ulong stream_index;
+};
+
+// vi: sw=4 ts=4 noet tw=80 cc=80