Replay export: videos are encoded at 100 Mbps whichever quality is chosen

he export dialog's quality setting picks 8, 12 or 18 Mbps for the H.264
encoder (three mov eax immediates at Skate.exe+0x62c24f, +0x62c256 and
+0x62c25d in the 20260929 build). All three are now 100000 kbps.
- The code around them must match the supported build's bytes, and each
  value must be the game's own or already 100000; otherwise nothing is
  written and the log says the change was skipped.
- The log says whether the bitrate change or the frame-pacing hook was
  skipped. start() reports success if either one is in place, so
  replay_export_initialized can say active while the hook is off.
- The fix that encodes every rendered frame is unchanged.
This commit is contained in:
AyeZeeBB committed 2026-10-14 20:11:01 -05:00
1 parent 84940ee571
commit 6df4f9a7bd
3 files changed
+75 -4

No files matched your search

@@ -11,6 +11,17 @@ inline constexpr std::uintptr_t submit_frame = 0x34f4610;
inline constexpr std::array<unsigned char, 24> 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<unsigned char, 34> 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<std::size_t, 3> bitrate_immediate_offsets{16, 23, 30};
inline constexpr std::array<std::uint32_t, 3> 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
+63 -4
View File
@@ -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 <array>
#include <atomic>
#include <cstring>
#include <Windows.h>
namespace dingosdk::replay_export {
namespace {
@@ -15,6 +19,52 @@ using Submit = void (*)(std::uintptr_t encoder, std::uintptr_t pixels, std::uint
std::atomic<Submit> original{};
std::atomic<std::uintptr_t> 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<void*>(address);
std::array<unsigned char, native::bitrate_switch_prefix.size()> 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(&current, 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<unsigned char, native::submit_frame_prefix.size()> 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<void*>(base + native::submit_frame);
void* previous{};
if (hook_prepare(target, reinterpret_cast<void*>(&submit), &previous) != HookOk) return false;
if (hook_prepare(target, reinterpret_cast<void*>(&submit), &previous) != HookOk) return bitrate;
original = reinterpret_cast<Submit>(previous);
if (hook_enable(target) != HookOk) {
hook_remove(target);
return false;
return bitrate;
}
return true;
} catch (...) {
+1
View File
@@ -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;
}