diff --git a/Engine/Game/Build/20260929/replay_export.h b/Engine/Game/Build/20260929/replay_export.h new file mode 100644 index 0000000..560d936 --- /dev/null +++ b/Engine/Game/Build/20260929/replay_export.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +// The replay editor's video export (analysis/replay-editor.md). +namespace dingosdk::game::build::v20260929::replay_export { +// The engine movie encoder's frame submit: (encoder, pixels, pitch). The render +// thread calls it at the end of every rendered frame while a capture runs, and +// it encodes the frame, video and its share of audio, before it returns. +inline constexpr std::uintptr_t submit_frame = 0x34f4610; +inline constexpr std::array submit_frame_prefix{ + 0x40,0x57,0x48,0x81,0xec,0x50,0x08,0x00,0x00,0x48,0x8b,0x05,0xa0,0xfd,0xcc,0x03, + 0x48,0x33,0xc4,0x48,0x89,0x84,0x24,0x20}; +// The encoder the replay editor exports with; the engine has others. +inline constexpr std::uintptr_t replay_encoder_vtable = 0x608c0b0; +// Encoder byte: a frame may be encoded. A world update sets it and an encoded +// frame clears it; submit_frame drops the frame it is called with while clear. +inline constexpr std::uintptr_t frame_permit = 0x90; +} diff --git a/Extension/Rendering/replay_export.cpp b/Extension/Rendering/replay_export.cpp new file mode 100644 index 0000000..158b6b8 --- /dev/null +++ b/Extension/Rendering/replay_export.cpp @@ -0,0 +1,48 @@ +#include "replay_export.h" +#include "Engine/Core/Hooks/hooks.h" +#include "Engine/Core/Platform/memory.h" +#include "Engine/Game/Build/addresses.h" +#include "Engine/Game/Build/20260929/replay_export.h" +#include +#include + +namespace dingosdk::replay_export { +namespace { +namespace native = addr::replay_export; +// The encoder passes its arguments straight on to the codec, so all four +// register arguments are forwarded as they came. +using Submit = void (*)(std::uintptr_t encoder, std::uintptr_t pixels, std::uintptr_t pitch, std::uintptr_t unused); +std::atomic original{}; +std::atomic image{}; + +void submit(std::uintptr_t encoder, std::uintptr_t pixels, std::uintptr_t pitch, std::uintptr_t unused) { + // Encoding is done on this thread before the call returns, so letting every + // frame through also holds the renderer to the pace the encoder keeps. + std::uintptr_t vtable{}; + if (memory::peek(encoder, vtable) && vtable == image.load() + native::replay_encoder_vtable) + *reinterpret_cast(encoder + native::frame_permit) = 1; + original.load()(encoder, pixels, pitch, unused); +} +} // namespace + +bool start(std::uintptr_t base) noexcept { + try { + std::array bytes{}; + if (!base || !memory::read_bytes(base + native::submit_frame, bytes.data(), bytes.size()) || + bytes != native::submit_frame_prefix) + return false; + image = base; + auto* target = reinterpret_cast(base + native::submit_frame); + void* previous{}; + if (hook_prepare(target, reinterpret_cast(&submit), &previous) != HookOk) return false; + original = reinterpret_cast(previous); + if (hook_enable(target) != HookOk) { + hook_remove(target); + return false; + } + return true; + } catch (...) { + return false; + } +} +} // namespace dingosdk::replay_export diff --git a/Extension/Rendering/replay_export.h b/Extension/Rendering/replay_export.h new file mode 100644 index 0000000..296ec21 --- /dev/null +++ b/Extension/Rendering/replay_export.h @@ -0,0 +1,11 @@ +#pragma once +#include + +namespace dingosdk::replay_export { +// The replay editor steps the replay by one frame of video for every frame the +// game renders, but the engine encodes at most one frame per world update and +// drops the rest. Rendering faster than it updates (an uncapped frame rate), +// an export came out with a fraction of its frames and played that much too +// fast. Every rendered frame of a replay export is encoded instead. +bool start(std::uintptr_t base) noexcept; +} diff --git a/Runtime/runtime_init.cpp b/Runtime/runtime_init.cpp index 568d910..06da626 100644 --- a/Runtime/runtime_init.cpp +++ b/Runtime/runtime_init.cpp @@ -33,6 +33,7 @@ #include "Extension/World/level_loading.h" #include "Extension/World/loading_screen.h" #include "Extension/Customization/preset_lookup_guard.h" +#include "Extension/Rendering/replay_export.h" #include "Engine/Vfs/content_catalogs.h" #include "Engine/Vfs/world_layer_scan.h" #include "Engine/Game/World/world_layer_catalog.h" @@ -372,6 +373,8 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() { record("{\"event\":\"map_loading_screens_initialized\",\"active\":" + std::string(loading_screens ? "true" : "false") + "}"); const bool preset_guard = dingosdk::preset_lookup_guard::start(r.base); record("{\"event\":\"preset_lookup_guard_initialized\",\"active\":" + std::string(preset_guard ? "true" : "false") + "}"); + const bool replay_export = dingosdk::replay_export::start(r.base); + record("{\"event\":\"replay_export_initialized\",\"active\":" + std::string(replay_export ? "true" : "false") + "}"); void* original{}; auto* address = reinterpret_cast(r.base + rt::client_tick); auto hook_status = dingosdk::hook_prepare(address, reinterpret_cast(&tick), &original); diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index 190c429..c8da6eb 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -87,6 +87,7 @@ add_library(dingosdk_runtime SHARED Extension/Assets/native_render_resource_pool.cpp Extension/World/native_entity_pages.cpp Extension/Rendering/display_startup.cpp + Extension/Rendering/replay_export.cpp Engine/Game/World/world_model.cpp Extension/World/level_loading.cpp Extension/World/loading_screen.cpp