shading parity program: shader source mirror, A/B capture, offline ucode emulation

This commit is contained in:
mchughalex 2026-07-06 23:23:13 +01:00
parent 70812600f7
commit 2dedfe1d6f
7 changed files with 755 additions and 91 deletions

View file

@ -699,6 +699,11 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
rex::ui::RegisterBind("bind_skate3_menu_alt", "F1", "Skate 3 settings alternate", [this] {
ToggleSimpleSettings();
});
// Remembered handle: the F11 paired A/B parity capture (native + emulated
// screenshots + gsnap, sequenced from the guest frame loop in
// skate3_native_render.cpp) needs the window without an app pointer.
skate3::screenshot::RememberWindow(window() ? window()->GetNativeWindowHandle()
: nullptr);
rex::ui::RegisterBind("bind_skate3_screenshot", "F6",
"Save screenshot to screenshots/", [this] {
skate3::screenshot::CaptureWindow(

View file

@ -1,6 +1,7 @@
#include "skate3_native_render.h"
#include "skate3_native_scene.h"
#include "skate3_screenshot.h"
#include "generated/skate3_init.h"
@ -9,6 +10,7 @@
#include <cinttypes>
#include <cstdint>
#include <cstdio>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <mutex>
@ -316,6 +318,74 @@ void OnFrameEnd(uint8_t* base) {
}
f10_was_down = f10_down;
}
// F11: paired A/B parity capture; one keypress produces
// shot_<ts>_native.png + shot_<ts>_emulated.png (same viewpoint, ~half a
// second apart while the renderer toggles) + an immediate F10-style
// capture taken back in native mode. Sequenced across guest frames so
// each renderer has settled before its screenshot (the emulated pipeline
// needs to recompose after suppression lifts).
{
enum class AbState { kIdle, kNativeSettle, kEmulatedSettle, kBackToNative };
static AbState ab_state = AbState::kIdle;
static uint64_t ab_resume_frame = 0;
static char ab_tag[24] = {};
static bool f11_was_down = false;
const bool f11_down = (GetAsyncKeyState(VK_F11) & 0x8000) != 0;
constexpr uint64_t kSettleFrames = 60;
switch (ab_state) {
case AbState::kIdle:
if (f11_down && !f11_was_down && !g_collecting) {
const std::time_t t = std::time(nullptr);
std::tm tm{};
localtime_s(&tm, &t);
std::snprintf(ab_tag, sizeof(ab_tag), "%02d%02d%02d", tm.tm_hour, tm.tm_min,
tm.tm_sec);
// Ensure the sequence starts in NATIVE mode (the gsnap capture at
// the end must record native-path scene data).
if (!skate3::native_scene::Enabled()) {
skate3::native_scene::ToggleSceneEnabled();
}
ab_state = AbState::kNativeSettle;
ab_resume_frame = g_frame_index + kSettleFrames;
REXLOG_INFO("native-render A/B capture: F11, tag {}", ab_tag);
}
break;
case AbState::kNativeSettle:
if (g_frame_index >= ab_resume_frame) {
char tag[40];
std::snprintf(tag, sizeof(tag), "%s_native", ab_tag);
skate3::screenshot::CaptureWindow(skate3::screenshot::RememberedWindow(), tag);
skate3::native_scene::ToggleSceneEnabled(); // -> emulated
ab_state = AbState::kEmulatedSettle;
ab_resume_frame = g_frame_index + kSettleFrames;
}
break;
case AbState::kEmulatedSettle:
if (g_frame_index >= ab_resume_frame) {
char tag[40];
std::snprintf(tag, sizeof(tag), "%s_emulated", ab_tag);
skate3::screenshot::CaptureWindow(skate3::screenshot::RememberedWindow(), tag);
skate3::native_scene::ToggleSceneEnabled(); // -> native
ab_state = AbState::kBackToNative;
ab_resume_frame = g_frame_index + kSettleFrames;
}
break;
case AbState::kBackToNative:
if (g_frame_index >= ab_resume_frame && !g_collecting) {
g_collecting = true;
g_immediate = true;
g_collect_counter = 0;
skate3::native_scene::StartRecording(1);
REXLOG_INFO(
"native-render A/B capture: screenshots tagged {} done, immediate "
"capture armed",
ab_tag);
ab_state = AbState::kIdle;
}
break;
}
f11_was_down = f11_down;
}
#endif
if (!g_collecting && g_frame_index % 32 == 0) {
const std::filesystem::path trigger = SnapshotDir() / "trigger";

File diff suppressed because it is too large Load diff

View file

@ -29,6 +29,13 @@ struct DrawItem {
uint32_t ib_count;
uint32_t diffuse_tex; // guest renderengine::Texture address (0 = none)
uint32_t lightmap_tex; // guest renderengine::Texture address (0 = none)
// "macrooverlay" material channel (environment shaders): a large-scale
// grime/crack overlay multiplied over the diffuse at uv *
// macroOverlayUVScale with macroOverlayOpacity: the ground/wall
// weathering that reads as "extra grit".
uint32_t macro_tex; // guest renderengine::Texture address (0 = none)
float macro_scale; // macroOverlayUVScale channel constant
float macro_opacity; // macroOverlayOpacity channel constant
uint16_t pos_offset;
uint16_t uv_offset;
uint16_t uv2_offset;
@ -48,6 +55,28 @@ struct DrawItem {
// by a per-character color staged in the PIXEL constant bank. Detected via
// the AttribulatorMaterialName channel; tint captured with the palette.
bool hair;
// AttribulatorMaterialName starts "environment.decal": wall/ground
// sections whose pixel shader composites a `decal` art texture (graffiti,
// painted branding, grime) over the base diffuse INSIDE the shader,
// lerp(base, decal.rgb, decal.a), and draws OPAQUE (disassembled from
// decalenvironment_defaultPS; these meshes ARE the surface there, so
// rendering them alpha-blended punches holes in walls). Without the
// composite the paint is missing (the no-graffiti bug).
bool decal;
uint32_t decal_art; // `decal` channel texture (0 = none)
// AttribulatorMaterialName starts "environment.transparent": mist/cloud
// sheets, glass, fences, vines. Alpha-BLENDED in a sub-pass after all
// opaque items (back-to-front, depth test on, z-write off); the opaque
// pass's alpha-test turned the soft mist gradients into solid hard-edged
// white cloud blobs.
bool transparent;
// character.cloth_ropa (Ropa cloth-simulated garments, e.g. player tees):
// the VS variant branches on a flag row kept in front of the bone palette
// - sim active means the dynamic VB already holds deformed root-local
// positions and the draw is RIGID (one affine at c188/c191), not skinned.
// Detected via the AttribulatorMaterialName channel; consumed by
// CaptureSkinnedState.
bool ropa;
float tint[4]; // rgb + enable flag in w
// The item's per-draw state (bone palette for skinned, world matrix for
// rigid model-space props) was not available at capture time; deferred
@ -77,6 +106,13 @@ struct FrameScene {
uint64_t generation = 0;
float view_proj[16] = {};
float cam_pos[3] = {};
// Global distance-fog parameter rows, captured once per frame from the
// first main-pass draw's VS constant bank (main-pass layout: c4 = camera,
// the validation key; c5 = fog ramp scale/bias/exponent, c6 = linear-space
// fog color rgb + transmittance scale in w; identical across every
// main-pass draw of a frame). Consumed by environment.transparent items.
float fog_ramp[4] = {0.0f, 0.0f, 1.0f, 0.0f};
float fog_color[4] = {};
std::vector<DrawItem> items;
};

View file

@ -7,6 +7,7 @@
#include <windows.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <ctime>
@ -59,15 +60,21 @@ bool PngEncoderClsid(CLSID* clsid) {
return false;
}
std::filesystem::path ScreenshotPath() {
std::filesystem::path ScreenshotPath(const char* tag) {
const auto now = std::chrono::system_clock::now();
const std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
localtime_s(&tm, &t);
char name[64];
char name[96];
if (tag != nullptr && tag[0] != '\0') {
std::snprintf(name, sizeof(name), "shot_%04d%02d%02d_%02d%02d%02d_%s.png",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
tm.tm_sec, tag);
} else {
std::snprintf(name, sizeof(name), "shot_%04d%02d%02d_%02d%02d%02d.png",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min,
tm.tm_sec);
}
return std::filesystem::absolute(std::filesystem::path("screenshots") / name);
}
@ -101,9 +108,17 @@ void EncodeAndSave(HBITMAP bitmap, int crop_x, int crop_y, int crop_w, int crop_
REXLOG_INFO("screenshot: saved {} ({}x{})", path.string(), crop_w, crop_h);
}
std::atomic<void*> g_remembered_window{nullptr};
} // namespace
void CaptureWindow(void* window_handle) {
void RememberWindow(void* hwnd) {
g_remembered_window.store(hwnd, std::memory_order_relaxed);
}
void* RememberedWindow() { return g_remembered_window.load(std::memory_order_relaxed); }
void CaptureWindow(void* window_handle, const char* tag) {
HWND hwnd = static_cast<HWND>(window_handle);
if (hwnd == nullptr) {
REXLOG_WARN("screenshot: no window handle");
@ -141,7 +156,7 @@ void CaptureWindow(void* window_handle) {
}
std::thread(EncodeAndSave, bitmap, int(client_origin.x - wr.left),
int(client_origin.y - wr.top), crop_w, crop_h, ScreenshotPath())
int(client_origin.y - wr.top), crop_w, crop_h, ScreenshotPath(tag))
.detach();
}
@ -150,9 +165,11 @@ void CaptureWindow(void* window_handle) {
#else // !_WIN32
namespace skate3::screenshot {
void CaptureWindow(void* /*window_handle*/) {
void CaptureWindow(void* /*window_handle*/, const char* /*tag*/) {
REXLOG_WARN("screenshot: only implemented on Windows");
}
void RememberWindow(void* /*hwnd*/) {}
void* RememberedWindow() { return nullptr; }
} // namespace skate3::screenshot
#endif // _WIN32

View file

@ -6,9 +6,17 @@ namespace skate3::screenshot {
// screenshots/ under the working directory (the install dir), logging the
// absolute path. Uses PrintWindow PW_RENDERFULLCONTENT, the flip-model
// swapchain reads black through a plain GDI BitBlt, and crops to the client
// area. The capture runs on the calling (UI) thread; the PNG encode runs on
// a detached worker so the frame hitch stays minimal. Windows only; a no-op
// with a warning elsewhere. hwnd is the platform-native window handle.
void CaptureWindow(void* hwnd);
// area. The capture runs on the calling thread (PrintWindow SendMessages to
// the UI thread, so any thread works); the PNG encode runs on a detached
// worker so the frame hitch stays minimal. Windows only; a no-op with a
// warning elsewhere. hwnd is the platform-native window handle. tag, when
// given, is appended to the filename (shot_<ts>_<tag>.png); the paired
// native/emulated A/B captures share one timestamp.
void CaptureWindow(void* hwnd, const char* tag = nullptr);
// The game window handle, stashed at dialog setup so frame-loop code (the
// F11 A/B parity capture) can screenshot without an app pointer.
void RememberWindow(void* hwnd);
void* RememberedWindow();
} // namespace skate3::screenshot

@ -1 +1 @@
Subproject commit c09b8c892d6038236def81e245dce6aef53aab19
Subproject commit 1161489933388807195b6ac65d471b8e45640eac