aboutsummaryrefslogtreecommitdiff
path: root/render.c
diff options
context:
space:
mode:
Diffstat (limited to 'render.c')
-rw-r--r--render.c199
1 files changed, 199 insertions, 0 deletions
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