diff --git a/Engine/Game/Build/20260929/replay_export.h b/Engine/Game/Build/20260929/replay_export.h index 560d936..0e09970 100644 --- a/Engine/Game/Build/20260929/replay_export.h +++ b/Engine/Game/Build/20260929/replay_export.h @@ -11,6 +11,17 @@ 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}; +// Maps the export dialog's 8/12/18 Mbps selection to the H.264 encoder rate. +// Raise all three outputs so export cannot silently fall back to the low-bitrate +// preset. The replacement rate is 100 Mbps (100000 kbps). +inline constexpr std::uintptr_t bitrate_switch = 0x62c240; +inline constexpr std::array bitrate_switch_prefix{ + 0x2b,0x85,0xc9,0x74,0x18,0x83,0xe9,0x01,0x74,0x05,0x83,0xf9,0x01,0x74,0x07,0xb8, + 0xe0,0x2e,0x00,0x00,0xeb,0x0c,0xb8,0x50,0x46,0x00,0x00,0xeb,0x05,0xb8,0x40,0x1f, + 0x00,0x00}; +inline constexpr std::array bitrate_immediate_offsets{16, 23, 30}; +inline constexpr std::array stock_bitrates_kbps{12000, 18000, 8000}; +inline constexpr std::uint32_t replay_bitrate_kbps = 100000; // 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 diff --git a/Extension/Rendering/replay_export.cpp b/Extension/Rendering/replay_export.cpp index 158b6b8..7ea4635 100644 --- a/Extension/Rendering/replay_export.cpp +++ b/Extension/Rendering/replay_export.cpp @@ -1,10 +1,14 @@ #include "replay_export.h" #include "Engine/Core/Hooks/hooks.h" +#include "Engine/Core/Log/logging.h" #include "Engine/Core/Platform/memory.h" #include "Engine/Game/Build/addresses.h" #include "Engine/Game/Build/20260929/replay_export.h" +#include "Engine/Game/Build/supported_build.h" #include #include +#include +#include namespace dingosdk::replay_export { namespace { @@ -15,6 +19,52 @@ using Submit = void (*)(std::uintptr_t encoder, std::uintptr_t pixels, std::uint std::atomic original{}; std::atomic image{}; +bool force_export_bitrate(std::uintptr_t base) noexcept { + const DWORD saved_error = GetLastError(); + const auto finish = [saved_error](bool result) { + SetLastError(saved_error); + return result; + }; + if (!base || native::bitrate_switch >= supported_build::game_image_size || + native::bitrate_switch_prefix.size() > supported_build::game_image_size - native::bitrate_switch) + return finish(false); + + const auto address = base + native::bitrate_switch; + auto* const destination = reinterpret_cast(address); + std::array actual{}; + if (!memory::read_bytes(address, actual.data(), actual.size())) return finish(false); + + bool needs_write{}; + for (std::size_t i = 0; i < actual.size(); ++i) { + bool bitrate_byte{}; + for (std::size_t slot = 0; slot < native::bitrate_immediate_offsets.size(); ++slot) { + const auto offset = native::bitrate_immediate_offsets[slot]; + if (i < offset || i >= offset + sizeof(std::uint32_t)) continue; + bitrate_byte = true; + if (i == offset) { + std::uint32_t current{}; + std::memcpy(¤t, actual.data() + offset, sizeof(current)); + if (current != native::stock_bitrates_kbps[slot] && current != native::replay_bitrate_kbps) + return finish(false); + needs_write |= current != native::replay_bitrate_kbps; + } + break; + } + if (!bitrate_byte && actual[i] != native::bitrate_switch_prefix[i]) return finish(false); + } + if (!needs_write) return finish(true); + + DWORD previous{}; + if (!VirtualProtect(destination, actual.size(), PAGE_EXECUTE_READWRITE, &previous)) return finish(false); + for (const auto offset : native::bitrate_immediate_offsets) + std::memcpy(actual.data() + offset, &native::replay_bitrate_kbps, sizeof(native::replay_bitrate_kbps)); + std::memcpy(destination, actual.data(), actual.size()); + const bool flushed = FlushInstructionCache(GetCurrentProcess(), destination, actual.size()) != FALSE; + DWORD discarded{}; + const bool restored = VirtualProtect(destination, actual.size(), previous, &discarded) != FALSE; + return finish(flushed && restored); +} + 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. @@ -27,18 +77,27 @@ void submit(std::uintptr_t encoder, std::uintptr_t pixels, std::uintptr_t pitch, bool start(std::uintptr_t base) noexcept { try { + const bool bitrate = force_export_bitrate(base); + logging::write(bitrate ? logging::Level::info : logging::Level::warning, + logging::Channel::graphics, bitrate + ? "Replay video export bitrate override active: 100 Mbps H.264." + : "Replay video export bitrate override skipped: game code did not match the supported build."); + std::array bytes{}; if (!base || !memory::read_bytes(base + native::submit_frame, bytes.data(), bytes.size()) || - bytes != native::submit_frame_prefix) - return false; + bytes != native::submit_frame_prefix) { + logging::write(logging::Level::warning, logging::Channel::graphics, + "Replay export frame pacing hook skipped: game code did not match the supported build."); + return bitrate; + } image = base; auto* target = reinterpret_cast(base + native::submit_frame); void* previous{}; - if (hook_prepare(target, reinterpret_cast(&submit), &previous) != HookOk) return false; + if (hook_prepare(target, reinterpret_cast(&submit), &previous) != HookOk) return bitrate; original = reinterpret_cast(previous); if (hook_enable(target) != HookOk) { hook_remove(target); - return false; + return bitrate; } return true; } catch (...) { diff --git a/Extension/Rendering/replay_export.h b/Extension/Rendering/replay_export.h index 296ec21..6d069e8 100644 --- a/Extension/Rendering/replay_export.h +++ b/Extension/Rendering/replay_export.h @@ -7,5 +7,6 @@ namespace dingosdk::replay_export { // 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. +// The export dialog's 8/12/18 Mbps choice is also raised to 100 Mbps H.264. bool start(std::uintptr_t base) noexcept; }