in-world neon spline waypoints, F5 live renderer toggle, F6 screenshots, compass icon fixes
This commit is contained in:
parent
d101dc8b78
commit
70812600f7
9 changed files with 1530 additions and 47 deletions
|
|
@ -229,6 +229,8 @@ set(SKATE3_COMMON_SOURCES
|
||||||
src/skate3_iso_installer.cpp
|
src/skate3_iso_installer.cpp
|
||||||
src/skate3_native_render.cpp
|
src/skate3_native_render.cpp
|
||||||
src/skate3_native_scene.cpp
|
src/skate3_native_scene.cpp
|
||||||
|
src/skate3_screenshot.cpp
|
||||||
|
src/skate3_shader_disasm.cpp
|
||||||
src/skate3_title_update_installer.cpp
|
src/skate3_title_update_installer.cpp
|
||||||
src/skate3_user_settings.cpp
|
src/skate3_user_settings.cpp
|
||||||
third_party/rexglue-sdk/thirdparty/crypto/sha256.cpp
|
third_party/rexglue-sdk/thirdparty/crypto/sha256.cpp
|
||||||
|
|
@ -295,7 +297,7 @@ if(APPLE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(skate3 PRIVATE comdlg32 winhttp d3dcompiler)
|
target_link_libraries(skate3 PRIVATE comdlg32 winhttp d3dcompiler gdiplus)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_custom_target(skate3_stage_runtime_library
|
add_custom_target(skate3_stage_runtime_library
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
#include "skate3_fov.h"
|
#include "skate3_fov.h"
|
||||||
#include "skate3_iso_installer.h"
|
#include "skate3_iso_installer.h"
|
||||||
#include "skate3_native_render.h"
|
#include "skate3_native_render.h"
|
||||||
|
#include "skate3_native_scene.h"
|
||||||
|
#include "skate3_screenshot.h"
|
||||||
|
#include "skate3_shader_disasm.h"
|
||||||
#include "skate3_title_update_installer.h"
|
#include "skate3_title_update_installer.h"
|
||||||
#include "skate3_user_settings.h"
|
#include "skate3_user_settings.h"
|
||||||
|
|
||||||
|
|
@ -60,6 +63,7 @@
|
||||||
#include <rex/system.h>
|
#include <rex/system.h>
|
||||||
#include <rex/ui/flags.h>
|
#include <rex/ui/flags.h>
|
||||||
#include <rex/ui/keybinds.h>
|
#include <rex/ui/keybinds.h>
|
||||||
|
#include <rex/ui/window.h>
|
||||||
#include <rex/ui/overlay/simple_settings_overlay.h>
|
#include <rex/ui/overlay/simple_settings_overlay.h>
|
||||||
#include <rex/ui/overlay/ultrawide_targets_overlay.h>
|
#include <rex/ui/overlay/ultrawide_targets_overlay.h>
|
||||||
|
|
||||||
|
|
@ -695,6 +699,15 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
|
||||||
rex::ui::RegisterBind("bind_skate3_menu_alt", "F1", "Skate 3 settings alternate", [this] {
|
rex::ui::RegisterBind("bind_skate3_menu_alt", "F1", "Skate 3 settings alternate", [this] {
|
||||||
ToggleSimpleSettings();
|
ToggleSimpleSettings();
|
||||||
});
|
});
|
||||||
|
rex::ui::RegisterBind("bind_skate3_screenshot", "F6",
|
||||||
|
"Save screenshot to screenshots/", [this] {
|
||||||
|
skate3::screenshot::CaptureWindow(
|
||||||
|
window() ? window()->GetNativeWindowHandle() : nullptr);
|
||||||
|
});
|
||||||
|
rex::ui::RegisterBind("bind_skate3_native_render_toggle", "F5",
|
||||||
|
"Toggle native/emulated renderer", [] {
|
||||||
|
skate3::native_scene::ToggleSceneEnabled();
|
||||||
|
});
|
||||||
rex::ui::RegisterBind("bind_skate3_ultrawide_targets", "F7",
|
rex::ui::RegisterBind("bind_skate3_ultrawide_targets", "F7",
|
||||||
"Skate 3 ultrawide targets", [this] {
|
"Skate 3 ultrawide targets", [this] {
|
||||||
ToggleUltrawideTargets();
|
ToggleUltrawideTargets();
|
||||||
|
|
@ -714,6 +727,7 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Skate3BaseApp::OnPostSetup() {
|
void Skate3BaseApp::OnPostSetup() {
|
||||||
|
skate3::shader_disasm::RunIfRequested();
|
||||||
ApplySelectedProfileToRuntime();
|
ApplySelectedProfileToRuntime();
|
||||||
ApplyGameplayCursorMode();
|
ApplyGameplayCursorMode();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,117 @@ extern "C" REX_FUNC(sub_82B7A458) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 2D / APT (Flash-converted HUD) reconnaissance hooks -----------------
|
||||||
|
// Every HUD/menu 2D element is a Flash SWF converted to EA APT, rendered by
|
||||||
|
// Sk8::FE::AptRenderingIntegration through the same guest D3D draw functions
|
||||||
|
// hooked above. These brackets tag draws issued inside the 2D pass so the
|
||||||
|
// recorder can capture and the future native 2D pass can replay them.
|
||||||
|
|
||||||
|
// Sk8::FE::FrontEndManager::Render2D(unsigned int): the game's whole 2D
|
||||||
|
// pass (FE movies + HUD).
|
||||||
|
extern "C" REX_FUNC(sub_825D9168) {
|
||||||
|
const bool enabled = skate3::native_render::Enabled();
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(0, true);
|
||||||
|
__imp__sub_825D9168(ctx, base);
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sk8::FE::AptMovieIntegration::Render(unsigned int): one APT movie.
|
||||||
|
extern "C" REX_FUNC(sub_825D67D8) {
|
||||||
|
const bool enabled = skate3::native_render::Enabled();
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(1, true);
|
||||||
|
__imp__sub_825D67D8(ctx, base);
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(1, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sk8::FE::AptRenderingIntegration::DrawRenderingUnit(void*, AptRenderInfo
|
||||||
|
// const*): one APT display-list element (texture quad / vector shape).
|
||||||
|
extern "C" REX_FUNC(sub_825D4490) {
|
||||||
|
const bool enabled = skate3::native_render::Enabled();
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(2, true);
|
||||||
|
__imp__sub_825D4490(ctx, base);
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(2, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sk8::FE::AptRenderingIntegration::UpdateRenderToTexture(unsigned int):
|
||||||
|
// in gameplay this renders the whole HUD into a screen-sized overlay
|
||||||
|
// texture at true screen coordinates (the game composites it later through
|
||||||
|
// the suppressed emulated pass). Diagnostic bracket only.
|
||||||
|
extern "C" REX_FUNC(sub_825D4E50) {
|
||||||
|
const bool enabled = skate3::native_render::Enabled();
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(3, true);
|
||||||
|
__imp__sub_825D4E50(ctx, base);
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(3, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sk8::Render::cFont::DrawstringLocal<char> / <unsigned short>: the glyph
|
||||||
|
// text emitter (trick names, scores). Text can flush OUTSIDE the APT
|
||||||
|
// brackets, so it gets its own bit.
|
||||||
|
extern "C" REX_FUNC(sub_82808388) {
|
||||||
|
const bool enabled = skate3::native_render::Enabled();
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(4, true);
|
||||||
|
__imp__sub_82808388(ctx, base);
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(4, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" REX_FUNC(sub_82808708) {
|
||||||
|
const bool enabled = skate3::native_render::Enabled();
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(4, true);
|
||||||
|
__imp__sub_82808708(ctx, base);
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(4, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sk8::Render::SimpleDraw::DrawParameters::Draw: the game's immediate-mode
|
||||||
|
// quad/tri utility (chase arrows, in-world guide markers/beams, debug
|
||||||
|
// draws). Bottoms out in BeginVertices like the APT path.
|
||||||
|
extern "C" REX_FUNC(sub_82804168) {
|
||||||
|
const bool enabled = skate3::native_render::Enabled();
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(5, true);
|
||||||
|
__imp__sub_82804168(ctx, base);
|
||||||
|
if (enabled) skate3::native_scene::On2dPhase(5, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// D3DDevice_SetPixelShader / SetVertexShader (r4 = guest shader object):
|
||||||
|
// recorded per draw to group the 2D stream by shader variant.
|
||||||
|
extern "C" REX_FUNC(sub_82B7F408) {
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_scene::OnSetShader(true, ctx.r4.u32);
|
||||||
|
}
|
||||||
|
__imp__sub_82B7F408(ctx, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" REX_FUNC(sub_82B7F150) {
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_scene::OnSetShader(false, ctx.r4.u32);
|
||||||
|
}
|
||||||
|
__imp__sub_82B7F150(ctx, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// D3D::SetPending_RenderStates(device, u64 dirty mask, bank, ptr): the
|
||||||
|
// render-state shadow bank (blend/depth state for 2D draws lives here).
|
||||||
|
extern "C" REX_FUNC(sub_82B83C48) {
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_scene::OnRenderStateUpload(ctx.r4.u64, ctx.r5.u32, ctx.r6.u32);
|
||||||
|
}
|
||||||
|
__imp__sub_82B83C48(ctx, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// D3DDevice_SetViewport(device, D3DVIEWPORT*) / SetScissorRect(device,
|
||||||
|
// RECT*): recorded per draw (render-to-texture APT passes and mask rects).
|
||||||
|
extern "C" REX_FUNC(sub_82B74310) {
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_scene::OnSetViewport(base, ctx.r4.u32);
|
||||||
|
}
|
||||||
|
__imp__sub_82B74310(ctx, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" REX_FUNC(sub_82B769C0) {
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_scene::OnSetScissor(base, ctx.r4.u32);
|
||||||
|
}
|
||||||
|
__imp__sub_82B769C0(ctx, base);
|
||||||
|
}
|
||||||
|
|
||||||
// D3DDevice_BeginVertices: inline (write-through-ring) vertex path; the
|
// D3DDevice_BeginVertices: inline (write-through-ring) vertex path; the
|
||||||
// CPU writes computed vertices directly. Post-call r3 = guest write pointer.
|
// CPU writes computed vertices directly. Post-call r3 = guest write pointer.
|
||||||
extern "C" REX_FUNC(sub_82B79FC0) {
|
extern "C" REX_FUNC(sub_82B79FC0) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -100,6 +100,35 @@ void OnRegisterTexture(uint64_t guid, uint32_t texture);
|
||||||
void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t ptr,
|
void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t ptr,
|
||||||
uint32_t device);
|
uint32_t device);
|
||||||
|
|
||||||
|
// 2D/APT phase bracket (the HUD and every other 2D element is a Flash SWF
|
||||||
|
// converted to EA APT, rendered through FE::AptRenderingIntegration). Guest
|
||||||
|
// draws issued while inside a bracket are 2D draws. bit: 0 =
|
||||||
|
// FrontEndManager::Render2D, 1 = AptMovieIntegration::Render, 2 =
|
||||||
|
// AptRenderingIntegration::DrawRenderingUnit, 3 =
|
||||||
|
// AptRenderingIntegration::UpdateRenderToTexture (in gameplay the whole HUD
|
||||||
|
// renders inside bit 3 into a screen-sized overlay texture at true screen
|
||||||
|
// coordinates; replaying those draws directly is the composite), 4 =
|
||||||
|
// Sk8::Render::cFont::DrawstringLocal (glyph text: trick names/scores;
|
||||||
|
// text can flush outside the APT brackets), 5 =
|
||||||
|
// Sk8::Render::SimpleDraw::DrawParameters::Draw (immediate-mode quads:
|
||||||
|
// chase arrows, in-world markers/beams).
|
||||||
|
void On2dPhase(uint32_t bit, bool enter);
|
||||||
|
|
||||||
|
// Current guest shader objects (D3DDevice_SetPixelShader/SetVertexShader),
|
||||||
|
// recorded per draw so offline analysis can group the 2D draw stream by
|
||||||
|
// shader variant.
|
||||||
|
void OnSetShader(bool pixel, uint32_t obj);
|
||||||
|
|
||||||
|
// D3D::SetPending_RenderStates(device, mask, bank, ptr): the render-state
|
||||||
|
// shadow bank (blend/depth state for the 2D pass lives here).
|
||||||
|
void OnRenderStateUpload(uint64_t mask, uint32_t bank, uint32_t ptr);
|
||||||
|
|
||||||
|
// D3DDevice_SetViewport / SetRenderState_ScissorTestEnable-adjacent scissor
|
||||||
|
// tracking, recorded per draw (render-to-texture APT passes show up as
|
||||||
|
// non-fullscreen viewports).
|
||||||
|
void OnSetViewport(uint8_t* base, uint32_t viewport_ptr);
|
||||||
|
void OnSetScissor(uint8_t* base, uint32_t rect_ptr);
|
||||||
|
|
||||||
// Guest buffer-binding trackers + post-draw state fixup: each
|
// Guest buffer-binding trackers + post-draw state fixup: each
|
||||||
// DrawIndexedVertices whose bound IB/VB match a pending item fills that
|
// DrawIndexedVertices whose bound IB/VB match a pending item fills that
|
||||||
// item's bone palette / world matrix from the constant bank (FIFO one-shot
|
// item's bone palette / world matrix from the constant bank (FIFO one-shot
|
||||||
|
|
@ -154,4 +183,12 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count);
|
||||||
// at startup regardless of cvar state.
|
// at startup regardless of cvar state.
|
||||||
void Install();
|
void Install();
|
||||||
|
|
||||||
|
// Runtime renderer switch (bound to F5): flips skate3_native_render_scene
|
||||||
|
// live. Capture hooks, scene build, guest-output replacement and
|
||||||
|
// emulated-draw suppression all re-check the cvar every frame, so the swap
|
||||||
|
// between the native renderer and the emulated GPU is seamless. Returns the
|
||||||
|
// new state; refuses (returns false) when the skate3_native_render hook
|
||||||
|
// layer was not enabled at boot.
|
||||||
|
bool ToggleSceneEnabled();
|
||||||
|
|
||||||
} // namespace skate3::native_scene
|
} // namespace skate3::native_scene
|
||||||
|
|
|
||||||
158
src/skate3_screenshot.cpp
Normal file
158
src/skate3_screenshot.cpp
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
#include "skate3_screenshot.h"
|
||||||
|
|
||||||
|
#include <rex/logging.h>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
// gdiplus.h uses unqualified min/max, which NOMINMAX removes.
|
||||||
|
namespace Gdiplus {
|
||||||
|
using std::max;
|
||||||
|
using std::min;
|
||||||
|
} // namespace Gdiplus
|
||||||
|
#include <objidl.h>
|
||||||
|
#include <gdiplus.h>
|
||||||
|
|
||||||
|
#ifndef PW_RENDERFULLCONTENT
|
||||||
|
#define PW_RENDERFULLCONTENT 0x00000002
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace skate3::screenshot {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool EnsureGdiplus() {
|
||||||
|
static bool initialized = [] {
|
||||||
|
Gdiplus::GdiplusStartupInput input;
|
||||||
|
ULONG_PTR token = 0;
|
||||||
|
return Gdiplus::GdiplusStartup(&token, &input, nullptr) == Gdiplus::Ok;
|
||||||
|
}();
|
||||||
|
return initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngEncoderClsid(CLSID* clsid) {
|
||||||
|
UINT count = 0;
|
||||||
|
UINT bytes = 0;
|
||||||
|
if (Gdiplus::GetImageEncodersSize(&count, &bytes) != Gdiplus::Ok || bytes == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string storage(bytes, '\0');
|
||||||
|
auto* codecs = reinterpret_cast<Gdiplus::ImageCodecInfo*>(storage.data());
|
||||||
|
if (Gdiplus::GetImageEncoders(count, bytes, codecs) != Gdiplus::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (UINT i = 0; i < count; ++i) {
|
||||||
|
if (wcscmp(codecs[i].MimeType, L"image/png") == 0) {
|
||||||
|
*clsid = codecs[i].Clsid;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path ScreenshotPath() {
|
||||||
|
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];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EncodeAndSave(HBITMAP bitmap, int crop_x, int crop_y, int crop_w, int crop_h,
|
||||||
|
std::filesystem::path path) {
|
||||||
|
if (!EnsureGdiplus()) {
|
||||||
|
REXLOG_ERROR("screenshot: GDI+ startup failed");
|
||||||
|
DeleteObject(bitmap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CLSID png;
|
||||||
|
if (!PngEncoderClsid(&png)) {
|
||||||
|
REXLOG_ERROR("screenshot: no PNG encoder");
|
||||||
|
DeleteObject(bitmap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Gdiplus::Bitmap full(bitmap, nullptr);
|
||||||
|
std::unique_ptr<Gdiplus::Bitmap> crop(
|
||||||
|
full.Clone(crop_x, crop_y, crop_w, crop_h, PixelFormat32bppRGB));
|
||||||
|
DeleteObject(bitmap);
|
||||||
|
if (!crop || crop->GetLastStatus() != Gdiplus::Ok) {
|
||||||
|
REXLOG_ERROR("screenshot: client-area crop failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(path.parent_path(), ec);
|
||||||
|
if (crop->Save(path.wstring().c_str(), &png, nullptr) != Gdiplus::Ok) {
|
||||||
|
REXLOG_ERROR("screenshot: PNG save failed for {}", path.string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
REXLOG_INFO("screenshot: saved {} ({}x{})", path.string(), crop_w, crop_h);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void CaptureWindow(void* window_handle) {
|
||||||
|
HWND hwnd = static_cast<HWND>(window_handle);
|
||||||
|
if (hwnd == nullptr) {
|
||||||
|
REXLOG_WARN("screenshot: no window handle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RECT wr{};
|
||||||
|
RECT cr{};
|
||||||
|
if (!GetWindowRect(hwnd, &wr) || !GetClientRect(hwnd, &cr)) {
|
||||||
|
REXLOG_WARN("screenshot: window rect query failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int win_w = wr.right - wr.left;
|
||||||
|
const int win_h = wr.bottom - wr.top;
|
||||||
|
const int crop_w = cr.right - cr.left;
|
||||||
|
const int crop_h = cr.bottom - cr.top;
|
||||||
|
if (win_w <= 0 || win_h <= 0 || crop_w <= 0 || crop_h <= 0) {
|
||||||
|
REXLOG_WARN("screenshot: window is zero-sized (minimized?)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
POINT client_origin{0, 0};
|
||||||
|
ClientToScreen(hwnd, &client_origin);
|
||||||
|
|
||||||
|
HDC screen_dc = GetDC(nullptr);
|
||||||
|
HDC mem_dc = CreateCompatibleDC(screen_dc);
|
||||||
|
HBITMAP bitmap = CreateCompatibleBitmap(screen_dc, win_w, win_h);
|
||||||
|
HGDIOBJ old = SelectObject(mem_dc, bitmap);
|
||||||
|
const BOOL printed = PrintWindow(hwnd, mem_dc, PW_RENDERFULLCONTENT);
|
||||||
|
SelectObject(mem_dc, old);
|
||||||
|
DeleteDC(mem_dc);
|
||||||
|
ReleaseDC(nullptr, screen_dc);
|
||||||
|
if (!printed) {
|
||||||
|
DeleteObject(bitmap);
|
||||||
|
REXLOG_WARN("screenshot: PrintWindow failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread(EncodeAndSave, bitmap, int(client_origin.x - wr.left),
|
||||||
|
int(client_origin.y - wr.top), crop_w, crop_h, ScreenshotPath())
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace skate3::screenshot
|
||||||
|
|
||||||
|
#else // !_WIN32
|
||||||
|
|
||||||
|
namespace skate3::screenshot {
|
||||||
|
void CaptureWindow(void* /*window_handle*/) {
|
||||||
|
REXLOG_WARN("screenshot: only implemented on Windows");
|
||||||
|
}
|
||||||
|
} // namespace skate3::screenshot
|
||||||
|
|
||||||
|
#endif // _WIN32
|
||||||
14
src/skate3_screenshot.h
Normal file
14
src/skate3_screenshot.h
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace skate3::screenshot {
|
||||||
|
|
||||||
|
// Captures the game window (F6) and writes a timestamped PNG to
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
} // namespace skate3::screenshot
|
||||||
71
src/skate3_shader_disasm.cpp
Normal file
71
src/skate3_shader_disasm.cpp
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Dev tool: offline Xenos ucode disassembly through the SDK's shader
|
||||||
|
// analyzer. When skate3_disasm_ucode_dir is set, every *.bin in that
|
||||||
|
// directory is treated as raw big-endian microcode (filename prefix vs_/ps_
|
||||||
|
// selects the shader type) and a .txt disassembly is written next to it at
|
||||||
|
// startup. Used to decode captured guest shaders (e.g. the spline/beam
|
||||||
|
// renderer) when porting draw paths to the native renderer.
|
||||||
|
|
||||||
|
#include "skate3_shader_disasm.h"
|
||||||
|
|
||||||
|
#include <bit>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <rex/cvar.h>
|
||||||
|
#include <rex/graphics/pipeline/shader/shader.h>
|
||||||
|
#include <rex/graphics/xenos.h>
|
||||||
|
#include <rex/logging.h>
|
||||||
|
#include <rex/string/buffer.h>
|
||||||
|
|
||||||
|
REXCVAR_DEFINE_STRING(skate3_disasm_ucode_dir, "", "Skate 3",
|
||||||
|
"Dev: disassemble every *.bin (raw big-endian Xenos microcode, "
|
||||||
|
"vs_/ps_ filename prefix = shader type) in this directory at "
|
||||||
|
"startup, writing <name>.txt beside each file")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||||
|
|
||||||
|
namespace skate3::shader_disasm {
|
||||||
|
|
||||||
|
void RunIfRequested() {
|
||||||
|
const std::string dir_str(REXCVAR_GET(skate3_disasm_ucode_dir));
|
||||||
|
if (dir_str.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::filesystem::path dir(dir_str);
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::is_directory(dir, ec)) {
|
||||||
|
REXLOG_WARN("shader-disasm: {} is not a directory", dir_str);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rex::string::StringBuffer disasm_buffer;
|
||||||
|
for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) {
|
||||||
|
if (entry.path().extension() != ".bin") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string name = entry.path().filename().string();
|
||||||
|
const bool pixel = name.rfind("ps_", 0) == 0;
|
||||||
|
std::ifstream in(entry.path(), std::ios::binary);
|
||||||
|
std::vector<char> bytes((std::istreambuf_iterator<char>(in)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
if (bytes.size() < 8 || (bytes.size() % 4) != 0) {
|
||||||
|
REXLOG_WARN("shader-disasm: {} skipped (size {})", name, bytes.size());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rex::graphics::Shader shader(
|
||||||
|
pixel ? rex::graphics::xenos::ShaderType::kPixel
|
||||||
|
: rex::graphics::xenos::ShaderType::kVertex,
|
||||||
|
0, reinterpret_cast<const uint32_t*>(bytes.data()), bytes.size() / 4,
|
||||||
|
std::endian::big);
|
||||||
|
shader.AnalyzeUcode(disasm_buffer);
|
||||||
|
std::filesystem::path out_path = entry.path();
|
||||||
|
out_path += ".txt";
|
||||||
|
std::ofstream out(out_path);
|
||||||
|
out << shader.ucode_disassembly();
|
||||||
|
REXLOG_INFO("shader-disasm: {} -> {} ({} bytes of disassembly)", name,
|
||||||
|
out_path.filename().string(), shader.ucode_disassembly().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace skate3::shader_disasm
|
||||||
9
src/skate3_shader_disasm.h
Normal file
9
src/skate3_shader_disasm.h
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace skate3::shader_disasm {
|
||||||
|
|
||||||
|
// If skate3_disasm_ucode_dir is set, disassembles every raw microcode *.bin
|
||||||
|
// in that directory to <name>.bin.txt. Call once at startup; no-op otherwise.
|
||||||
|
void RunIfRequested();
|
||||||
|
|
||||||
|
} // namespace skate3::shader_disasm
|
||||||
Loading…
Add table
Reference in a new issue