diff --git a/src/skate3_app_common.cpp b/src/skate3_app_common.cpp index d4300bc..32986f6 100644 --- a/src/skate3_app_common.cpp +++ b/src/skate3_app_common.cpp @@ -770,7 +770,7 @@ void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) { "Native render debug menu", [this] { ToggleNativeDebug(); }); - rex::ui::RegisterBind("bind_skate3_showcase", "Home", + rex::ui::RegisterBind("bind_skate3_showcase", "Ctrl+Shift+B", "Graphics build-up showcase", [] { REXCVAR_SET( skate3_native_render_scene_showcase, diff --git a/src/skate3_native_debug_dialog.cpp b/src/skate3_native_debug_dialog.cpp index 71d1307..7e00827 100644 --- a/src/skate3_native_debug_dialog.cpp +++ b/src/skate3_native_debug_dialog.cpp @@ -426,7 +426,7 @@ void DrawShowcaseSetupWindow(bool* p_open) { return; } const bool running = REXCVAR_GET(skate3_native_render_scene_showcase); - if (ImGui::Button(running ? "Cancel showcase" : "Start showcase (Home)", + if (ImGui::Button(running ? "Cancel showcase" : "Start showcase (Ctrl+Shift+B)", ImVec2(-1.0f, 0.0f))) { REXCVAR_SET(skate3_native_render_scene_showcase, !running); } @@ -551,7 +551,7 @@ void DrawShowcaseCaptureSection() { DrawMaxQualityToggle(); { const bool running = REXCVAR_GET(skate3_native_render_scene_showcase); - if (ImGui::Button(running ? "Cancel showcase" : "Start showcase (Home)")) { + if (ImGui::Button(running ? "Cancel showcase" : "Start showcase (Ctrl+Shift+B)")) { REXCVAR_SET(skate3_native_render_scene_showcase, !running); } if (ImGui::IsItemHovered()) { diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 3ca1181..e020fc6 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -300,8 +300,8 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_showcase, false, "Skate 3", "both sides rendered live. Layer order and grouping come from " "skate3_native_render_scene_showcase_order (edited in the F12 " "showcase setup window). Clears itself when the sequence " - "finishes; set to false mid-run to cancel. Also on the Home " - "key by default (bind_skate3_showcase). Layers whose feature " + "finishes; set to false mid-run to cancel. Also on " + "Ctrl+Shift+B by default (bind_skate3_showcase). Layers whose feature " "is disabled (or unavailable without the HDR intermediate) " "are skipped.") .lifecycle(rex::cvar::Lifecycle::kHotReload); @@ -7883,6 +7883,43 @@ static void ApplySunOverride(FrameScene& scene) { } } +// Hor+ ultrawide: clip-space X scale that widens the game's screen-shaped +// projection to the wide guest-output aspect (see the SDK's +// ApplyNativeGuestOutputWideAspect). 1.0 while the output is not wide. The +// projection aspect is read off the matrix itself (|m11/m00|) so the FOV +// override and freecam zoom compose naturally. +float WideOutputHorScale(const float proj[16]) { + const double wide_aspect = rex::graphics::GetNativeGuestOutputWideAspect(); + if (wide_aspect <= 0.0 || !rex::graphics::IsNativeGuestOutputActive()) { + return 1.0f; + } + const float m00 = std::fabs(proj[0]); + const float m11 = std::fabs(proj[5]); + if (!(m00 > 1e-6f) || !(m11 > 1e-6f)) { + return 1.0f; + } + const double base_aspect = double(m11) / double(m00); + if (wide_aspect <= base_aspect + 0.01) { + return 1.0f; + } + return float(base_aspect / wide_aspect); +} + +// Applies the Hor+ scale to the published camera: column 0 of the row-vector +// projection and view*proj (clip.x = dot(v, column 0)). Every consumer (draw +// MVPs, post-pass unprojection, splines, frustum tests) reads these published +// matrices, so the frame stays self-consistent, exactly as if the game camera +// itself had the wide aspect. +void WidenPublishedCamera(FrameScene& scene, float scale) { + if (scale == 1.0f) { + return; + } + for (int r = 0; r < 4; ++r) { + scene.proj[r * 4 + 0] *= scale; + scene.view_proj[r * 4 + 0] *= scale; + } +} + void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { if (!SceneEnabled()) { return; @@ -9218,6 +9255,12 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { } } + // Hor+ ultrawide scale for this frame's camera (1.0 when the output is not + // wide). The matrices are widened after the camera overrides below; the + // retention frustum guards divide by it here so their tests already match + // the frame's real (wider) view. + const float wide_hor_scale = WideOutputHorScale(scene.proj); + // Off-screen retention (see g_retained_items): re-append statics the game // view-culled this frame while the trailing rendered pose can still see // them. Runs AFTER the smoothing block so retained copies never enter the @@ -9275,14 +9318,14 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { // it. The 0.97 margin shrinks the tested frustum so bounds poking // just inside an edge still count as view-culled. if (now - r.last_seen > kRetainTtlFrames || - !ItemOutsideFrustum(r.item, guest_vp, 0.97f)) { + !ItemOutsideFrustum(r.item, guest_vp, 0.97f / wide_hor_scale)) { rit = g_retained_items.erase(rit); continue; } // Draw it only if the RENDERED pose can actually see it (widened // frustum: only clearly-outside skips); after a fast 180 the trail // behind the camera stays retained but costs nothing. - if (!ItemOutsideFrustum(r.item, scene.view_proj, 1.05f)) { + if (!ItemOutsideFrustum(r.item, scene.view_proj, 1.05f / wide_hor_scale)) { scene.items.push_back(r.item); } ++rit; @@ -9478,6 +9521,11 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { UpdateFreecam(scene, cam_view, std::chrono::duration(build_t0.time_since_epoch()).count()); + // Hor+ ultrawide: widen the published camera to the wide output aspect. + // Applied after every camera override (smoothing, synthetic pan, freecam) + // so it survives their view_proj/proj rewrites. + WidenPublishedCamera(scene, wide_hor_scale); + // Publish the frame's captured fog rows and re-arm the OnDrawDone capture // (keyed to this frame's camera) for the next frame. if (g_fog_have) {