render: showcase decals/dyn layers, blackout bookends, and hot-rebuild fixes
This commit is contained in:
parent
0cdead0283
commit
33b7465d2e
9 changed files with 15566 additions and 15216 deletions
|
|
@ -148,13 +148,25 @@ VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1
|
||||||
// 1 albedo textures 2 baked lighting 4 materials & surface detail
|
// 1 albedo textures 2 baked lighting 4 materials & surface detail
|
||||||
// 8 dynamic shadows 16 ambient occlusion 32 reflections (SSR)
|
// 8 dynamic shadows 16 ambient occlusion 32 reflections (SSR)
|
||||||
// 64 volumetrics 128 bloom
|
// 64 volumetrics 128 bloom
|
||||||
|
// 256 decals & grime 512 dynamic entities
|
||||||
|
// 1024 blackout: the run's opening/closing bookends (ps_main renders every
|
||||||
|
// draw black; doubles as the recording cut marker), not an orderable layer.
|
||||||
// The material bits are progressive looks (materials subsumes lighting
|
// The material bits are progressive looks (materials subsumes lighting
|
||||||
// subsumes albedo); the rest are independent, so the sequencer can reveal
|
// subsumes albedo); the rest are independent, so the sequencer can reveal
|
||||||
// them in any order or grouping. Returns -1 when the showcase is off.
|
// them in any order or grouping. 256/512 are SUBTRACTIVE layers: their
|
||||||
|
// content belongs to the normal frame and stays hidden until the bit
|
||||||
|
// reveals (the sequencer folds their bits into every step when the layer
|
||||||
|
// is not part of the run). Returns -1 when the showcase is off.
|
||||||
int ShowcaseMask(float px_x) {
|
int ShowcaseMask(float px_x) {
|
||||||
float v = px_x < sh_v2.w ? sh_v2.y : sh_v2.z;
|
float v = px_x < sh_v2.w ? sh_v2.y : sh_v2.z;
|
||||||
return v < 255.5 ? -1 : (int)(v + 0.5) - 256;
|
return v < 255.5 ? -1 : (int)(v + 0.5) - 256;
|
||||||
}
|
}
|
||||||
|
// True when the given layer bit is revealed on this pixel's side of the
|
||||||
|
// split (or the showcase is off entirely).
|
||||||
|
bool ShowcaseLayerOn(float px_x, int bit) {
|
||||||
|
int m = ShowcaseMask(px_x);
|
||||||
|
return m < 0 || (m & bit) != 0;
|
||||||
|
}
|
||||||
#include "scene_shadows.hlsli"
|
#include "scene_shadows.hlsli"
|
||||||
#include "scene_common.hlsli"
|
#include "scene_common.hlsli"
|
||||||
#include "scene_char.hlsli"
|
#include "scene_char.hlsli"
|
||||||
|
|
@ -292,12 +304,15 @@ float4 ShadePixel(VSOut i) {
|
||||||
// against (CSM s + shadow color), kd, phong spec vs the shader's
|
// against (CSM s + shadow color), kd, phong spec vs the shader's
|
||||||
// fixed literal light, cube reflection on 5/6.
|
// fixed literal light, cube reflection on 5/6.
|
||||||
float3 ov = float3(1.0, 1.0, 1.0);
|
float3 ov = float3(1.0, 1.0, 1.0);
|
||||||
|
// Decals-&-grime build-up layer (bit 256): macro weathering and decal
|
||||||
|
// art both render neutral until revealed.
|
||||||
|
bool sc_decals = ShowcaseLayerOn(i.pos.x, 256);
|
||||||
// Fam 13 (transparentenvironmentreflective) carries no macro term.
|
// Fam 13 (transparentenvironmentreflective) carries no macro term.
|
||||||
if (overlay.z > 0.0 && fam < 12.5) {
|
if (overlay.z > 0.0 && fam < 12.5 && sc_decals) {
|
||||||
float3 mo = macro.Sample(smp, i.uv * overlay.x).rgb;
|
float3 mo = macro.Sample(smp, i.uv * overlay.x).rgb;
|
||||||
ov = saturate((mo - 0.5) * overlay.y + 0.5);
|
ov = saturate((mo - 0.5) * overlay.y + 0.5);
|
||||||
}
|
}
|
||||||
if (fam > 2.5 && fam < 4.5 && overlay.w > 0.5) {
|
if (fam > 2.5 && fam < 4.5 && overlay.w > 0.5 && sc_decals) {
|
||||||
// overlay.w == 0 = art unresolved (white fallback alpha 1 would
|
// overlay.w == 0 = art unresolved (white fallback alpha 1 would
|
||||||
// whitewash the whole surface).
|
// whitewash the whole surface).
|
||||||
float4 dk = overlay.w > 1.5 ? decal_art.Sample(smp, i.uv3)
|
float4 dk = overlay.w > 1.5 ? decal_art.Sample(smp, i.uv3)
|
||||||
|
|
@ -553,7 +568,10 @@ float4 ShadePixel(VSOut i) {
|
||||||
// macro overlay: the weathering applies over the paint too
|
// macro overlay: the weathering applies over the paint too
|
||||||
// (ApplyOverlay(cOverlay, ApplyDecal(...)); the old order left the paint
|
// (ApplyOverlay(cOverlay, ApplyDecal(...)); the old order left the paint
|
||||||
// unweathered, the too-white ramp stencils).
|
// unweathered, the too-white ramp stencils).
|
||||||
if (overlay.w > 0.0) {
|
// Decals-&-grime build-up layer (bit 256): art and macro weathering both
|
||||||
|
// render neutral until revealed.
|
||||||
|
bool sc_decals = ShowcaseLayerOn(i.pos.x, 256);
|
||||||
|
if (overlay.w > 0.0 && sc_decals) {
|
||||||
// overlay.w == 2 marks environment.decal_tileable: the art tiles across
|
// overlay.w == 2 marks environment.decal_tileable: the art tiles across
|
||||||
// the surface (rock/cliff faces) and must WRAP; clamp stretched the
|
// the surface (rock/cliff faces) and must WRAP; clamp stretched the
|
||||||
// border texels into giant streaks. Single placements clamp (their
|
// border texels into giant streaks. Single placements clamp (their
|
||||||
|
|
@ -569,7 +587,8 @@ float4 ShadePixel(VSOut i) {
|
||||||
// linear (squared) color space, so the gamma-space equivalent is
|
// linear (squared) color space, so the gamma-space equivalent is
|
||||||
// sqrt(m); a direct multiply doubles the darkening (harsh black
|
// sqrt(m); a direct multiply doubles the darkening (harsh black
|
||||||
// patchwork vs the emulated subtle weathering).
|
// patchwork vs the emulated subtle weathering).
|
||||||
if (overlay.z > 0.0 && misc.x < 1.5) { // water reuses overlay.z (ripple map flag)
|
if (overlay.z > 0.0 && misc.x < 1.5 && // water reuses overlay.z (ripple map flag)
|
||||||
|
sc_decals) {
|
||||||
float4 m = macro.Sample(smp, i.uv * overlay.x);
|
float4 m = macro.Sample(smp, i.uv * overlay.x);
|
||||||
albedo.rgb *= lerp(float3(1.0, 1.0, 1.0), sqrt(m.rgb), overlay.y * m.a);
|
albedo.rgb *= lerp(float3(1.0, 1.0, 1.0), sqrt(m.rgb), overlay.y * m.a);
|
||||||
}
|
}
|
||||||
|
|
@ -701,8 +720,22 @@ float4 ShadePixel(VSOut i) {
|
||||||
return float4(PassGamma(lit), 1.0);
|
return float4(PassGamma(lit), 1.0);
|
||||||
}
|
}
|
||||||
float4 ps_main(VSOut i) : SV_Target {
|
float4 ps_main(VSOut i) : SV_Target {
|
||||||
float4 c = ShadePixel(i);
|
|
||||||
int mask = ShowcaseMask(i.pos.x);
|
int mask = ShowcaseMask(i.pos.x);
|
||||||
|
// Dynamic-entities build-up layer (bit 512): entity draws (tint.a < 0,
|
||||||
|
// staged CPU-side from DrawItem::dyn_entity) drop out entirely until the
|
||||||
|
// bit reveals on this pixel's side of the split; no color and no depth,
|
||||||
|
// so the world behind them renders as if they were never published.
|
||||||
|
if (mask >= 0 && (mask & 512) == 0 && tint.a < -0.5) {
|
||||||
|
clip(-1.0);
|
||||||
|
return float4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
float4 c = ShadePixel(i);
|
||||||
|
// Blackout stage (bit 1024, the showcase's opening/closing bookends and
|
||||||
|
// the recording cut marker): every draw renders black, keeping its own
|
||||||
|
// alpha so coverage, alpha test and blend routing stay intact.
|
||||||
|
if (mask >= 0 && (mask & 1024) != 0) {
|
||||||
|
return float4(0.0, 0.0, 0.0, c.a);
|
||||||
|
}
|
||||||
if (mask < 0 || (mask & 4) != 0) {
|
if (mask < 0 || (mask & 4) != 0) {
|
||||||
return c; // showcase off, or the full material layer is revealed
|
return c; // showcase off, or the full material layer is revealed
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -11,7 +11,10 @@ cbuffer C : register(b0) {
|
||||||
float4 vp1;
|
float4 vp1;
|
||||||
float4 vp2;
|
float4 vp2;
|
||||||
float4 vp3;
|
float4 vp3;
|
||||||
float4 intensity; // i_intensity as staged (x = default gain, y = darken gain)
|
float4 intensity; // x/y = i_intensity as staged (default/darken gain).
|
||||||
|
// zw = showcase blackout gate, staged CPU-side over the
|
||||||
|
// guest row's unused lanes: z = split x (output pixels),
|
||||||
|
// w = left|right visibility bits (3 = fully visible).
|
||||||
};
|
};
|
||||||
Texture2D<float4> tex : register(t0);
|
Texture2D<float4> tex : register(t0);
|
||||||
SamplerState smp : register(s0);
|
SamplerState smp : register(s0);
|
||||||
|
|
@ -48,12 +51,25 @@ float3 SplineOut(float3 c) {
|
||||||
return c;
|
return c;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
// Showcase blackout gate: the spline pass draws outside the scene shaders,
|
||||||
|
// so ps_main's blackout stage (scene.hlsl, mask bit 1024) cannot cover it;
|
||||||
|
// without this the neon lines leak over the black recording bookends.
|
||||||
|
bool SplineVisible(float px_x) {
|
||||||
|
int bits = (int)(intensity.w + 0.5);
|
||||||
|
return ((px_x < intensity.z ? 1 : 2) & bits) != 0;
|
||||||
|
}
|
||||||
float4 ps_default(VSOut i) : SV_Target {
|
float4 ps_default(VSOut i) : SV_Target {
|
||||||
|
if (!SplineVisible(i.pos.x)) {
|
||||||
|
clip(-1.0);
|
||||||
|
}
|
||||||
float4 c = tex.Sample(smp, i.uv);
|
float4 c = tex.Sample(smp, i.uv);
|
||||||
float3 sq = c.rgb * c.rgb * (intensity.x * i.fade / max(c.a, 1.0 / 255.0));
|
float3 sq = c.rgb * c.rgb * (intensity.x * i.fade / max(c.a, 1.0 / 255.0));
|
||||||
return float4(SplineOut(sqrt(abs(sq))), 1.0);
|
return float4(SplineOut(sqrt(abs(sq))), 1.0);
|
||||||
}
|
}
|
||||||
float4 ps_darken(VSOut i) : SV_Target {
|
float4 ps_darken(VSOut i) : SV_Target {
|
||||||
|
if (!SplineVisible(i.pos.x)) {
|
||||||
|
clip(-1.0);
|
||||||
|
}
|
||||||
float4 c = tex.Sample(smp, i.uv);
|
float4 c = tex.Sample(smp, i.uv);
|
||||||
float3 rgb = sqrt(abs(c.rgb * c.rgb * intensity.y));
|
float3 rgb = sqrt(abs(c.rgb * c.rgb * intensity.y));
|
||||||
float a = sqrt(abs(c.a * intensity.y * i.fade));
|
float a = sqrt(abs(c.a * intensity.y * i.fade));
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
#include <rex/cvar.h>
|
#include <rex/cvar.h>
|
||||||
#include <rex/graphics/native_guest_renderer.h>
|
#include <rex/graphics/native_guest_renderer.h>
|
||||||
|
#include <rex/logging.h>
|
||||||
#include <rex/ui/presenter.h>
|
#include <rex/ui/presenter.h>
|
||||||
|
|
||||||
#include "skate3_native_scene.h"
|
#include "skate3_native_scene.h"
|
||||||
|
|
@ -254,6 +255,14 @@ void SetMaxQuality(bool enable) {
|
||||||
s.active = enable;
|
s.active = enable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REXCVAR_DEFINE_INT32(
|
||||||
|
skate3_native_render_scene_maxq_cycle, 0, "Skate 3",
|
||||||
|
"DEBUG: auto-toggle MAX QUALITY every N native-scene frames (0 = off). "
|
||||||
|
"Stress-tests the hot pipeline/target/shadow-map rebuild path; pair "
|
||||||
|
"with Vulkan validation layers when hunting stale bindings.")
|
||||||
|
.range(0, 1000000)
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
|
||||||
void DrawMaxQualityToggle() {
|
void DrawMaxQualityToggle() {
|
||||||
bool on = s_max_quality.active;
|
bool on = s_max_quality.active;
|
||||||
if (ImGui::Checkbox("MAX QUALITY (photo mode)", &on)) {
|
if (ImGui::Checkbox("MAX QUALITY (photo mode)", &on)) {
|
||||||
|
|
@ -1096,6 +1105,24 @@ void DrawCachesSection() {
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void MaxQualityAutoCycle(uint64_t frame_number) {
|
||||||
|
const int32_t period = REXCVAR_GET(skate3_native_render_scene_maxq_cycle);
|
||||||
|
if (period <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static uint64_t next_frame = 0;
|
||||||
|
if (next_frame == 0) {
|
||||||
|
next_frame = frame_number + uint64_t(period);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (frame_number >= next_frame) {
|
||||||
|
next_frame = frame_number + uint64_t(period);
|
||||||
|
SetMaxQuality(!s_max_quality.active);
|
||||||
|
REXLOG_INFO("native-scene: max-quality auto-cycle -> {}",
|
||||||
|
s_max_quality.active ? "ON" : "OFF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void NativeDebugDialog::Show() {
|
void NativeDebugDialog::Show() {
|
||||||
visible_ = true;
|
visible_ = true;
|
||||||
SetDrawActive(true);
|
SetDrawActive(true);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@
|
||||||
|
|
||||||
namespace skate3 {
|
namespace skate3 {
|
||||||
|
|
||||||
|
// Debug stress driver: when skate3_native_render_scene_maxq_cycle > 0,
|
||||||
|
// toggles the max-quality photo mode every N native-scene frames. Called
|
||||||
|
// once per frame from the scene renderer (works without the F12 dialog).
|
||||||
|
void MaxQualityAutoCycle(uint64_t frame_number);
|
||||||
|
|
||||||
class NativeDebugDialog final : public rex::ui::ImGuiDialog {
|
class NativeDebugDialog final : public rex::ui::ImGuiDialog {
|
||||||
public:
|
public:
|
||||||
explicit NativeDebugDialog(rex::ui::ImGuiDrawer* drawer) : ImGuiDialog(drawer) {}
|
explicit NativeDebugDialog(rex::ui::ImGuiDrawer* drawer) : ImGuiDialog(drawer) {}
|
||||||
|
|
|
||||||
|
|
@ -294,10 +294,13 @@ REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_haze_density, 0.005,
|
||||||
.range(0.0, 0.05)
|
.range(0.0, 0.05)
|
||||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_showcase, false, "Skate 3",
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_showcase, false, "Skate 3",
|
||||||
"Run the graphics build-up showcase: the scene is stripped to "
|
"Run the graphics build-up showcase: the screen snaps to "
|
||||||
"flat clay geometry, then rebuilt layer by layer, each layer "
|
"black, flat clay geometry wipes in, the scene is rebuilt "
|
||||||
"revealed by a vertical split wiping across the screen with "
|
"layer by layer (each layer revealed by a vertical split "
|
||||||
"both sides rendered live. Layer order and grouping come from "
|
"wiping across the screen with both sides rendered live), "
|
||||||
|
"and the run closes by wiping back to black. The black "
|
||||||
|
"bookends double as cut markers for screen recordings. "
|
||||||
|
"Layer order and grouping come from "
|
||||||
"skate3_native_render_scene_showcase_order (edited in the F12 "
|
"skate3_native_render_scene_showcase_order (edited in the F12 "
|
||||||
"showcase setup window). Clears itself when the sequence "
|
"showcase setup window). Clears itself when the sequence "
|
||||||
"finishes; set to false mid-run to cancel. Also on "
|
"finishes; set to false mid-run to cancel. Also on "
|
||||||
|
|
@ -310,10 +313,16 @@ REXCVAR_DEFINE_STRING(
|
||||||
skate3::native_scene::kShowcaseOrderDefault, "Skate 3",
|
skate3::native_scene::kShowcaseOrderDefault, "Skate 3",
|
||||||
"Showcase layer order: comma-separated reveal steps, '+' joins layers "
|
"Showcase layer order: comma-separated reveal steps, '+' joins layers "
|
||||||
"into one wipe, a '-' prefix disables a layer while keeping its "
|
"into one wipe, a '-' prefix disables a layer while keeping its "
|
||||||
"position. Tokens: albedo, lighting, materials, shadows, ao, ssr, vol, "
|
"position. Tokens: albedo, lighting, materials, decals, dyn, shadows, "
|
||||||
"bloom (the run always starts from clay geometry, and a final "
|
"ao, ssr, vol, bloom (the run always starts from clay geometry, and a "
|
||||||
"full-render step is appended when the list leaves layers unrevealed). "
|
"final full-render step is appended when the list leaves layers "
|
||||||
"Example: \"albedo,lighting,materials,shadows+ao,-ssr,vol+bloom\".")
|
"unrevealed). decals (graffiti/paint art + macro weathering; renders on "
|
||||||
|
"the full material look, so place it after materials) and dyn (dynamic "
|
||||||
|
"entities: characters, props, cloth) HIDE their content until revealed; "
|
||||||
|
"left out or disabled, that content renders from clay onward as usual. "
|
||||||
|
"The CSM atlas is shared by both sides of the split, so a dyn step "
|
||||||
|
"AFTER shadows shows entity shadows before their owners; order dyn "
|
||||||
|
"first. Example: \"albedo,lighting,materials,decals,dyn,shadows+ao\".")
|
||||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_showcase_hold, 2.5, "Skate 3",
|
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_showcase_hold, 2.5, "Skate 3",
|
||||||
"Showcase: seconds each build-up stage holds fullscreen "
|
"Showcase: seconds each build-up stage holds fullscreen "
|
||||||
|
|
@ -4100,6 +4109,7 @@ uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx, bool world_path,
|
||||||
}
|
}
|
||||||
item.shadow_caster =
|
item.shadow_caster =
|
||||||
ortho_submit || (ctx != 0 && g_frame_ortho_ctx.count(ctx) != 0);
|
ortho_submit || (ctx != 0 && g_frame_ortho_ctx.count(ctx) != 0);
|
||||||
|
item.dyn_entity = true;
|
||||||
g_frame_dynitems.push_back(std::move(item));
|
g_frame_dynitems.push_back(std::move(item));
|
||||||
const size_t index = g_frame_dynitems.size() - 1;
|
const size_t index = g_frame_dynitems.size() - 1;
|
||||||
if (g_frame_dynitems[index].pending ||
|
if (g_frame_dynitems[index].pending ||
|
||||||
|
|
@ -4204,6 +4214,7 @@ uint32_t CaptureClothDraw(uint8_t* base, uint32_t r4, uint32_t r5, uint32_t r6,
|
||||||
|
|
||||||
// Distinct draw ranges within one ring buffer are distinct garments.
|
// Distinct draw ranges within one ring buffer are distinct garments.
|
||||||
*out_key = garment_key;
|
*out_key = garment_key;
|
||||||
|
item.dyn_entity = true;
|
||||||
std::lock_guard<std::mutex> lock(g_palette_mutex);
|
std::lock_guard<std::mutex> lock(g_palette_mutex);
|
||||||
g_frame_dynitems.push_back(std::move(item));
|
g_frame_dynitems.push_back(std::move(item));
|
||||||
return uint32_t(g_frame_dynitems.size());
|
return uint32_t(g_frame_dynitems.size());
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,11 @@ struct DrawItem {
|
||||||
// only their exact cached decodes: no guest-memory reads, no heals, no
|
// only their exact cached decodes: no guest-memory reads, no heals, no
|
||||||
// miss enqueues.
|
// miss enqueues.
|
||||||
bool retained = false;
|
bool retained = false;
|
||||||
|
// Captured through the dynamic-entity paths (characters, props, cloth,
|
||||||
|
// quad-list garments) rather than the world sort lists. Set at capture so
|
||||||
|
// every later copy (merges, rescues, retention re-publishes) inherits it;
|
||||||
|
// the build-up showcase's dynamic-entities layer keys off it.
|
||||||
|
bool dyn_entity = false;
|
||||||
// Bone palette snapshot taken on the game thread: raw staged rows, 3
|
// Bone palette snapshot taken on the game thread: raw staged rows, 3
|
||||||
// float4s per bone (column-vector affine [R | t], model -> world). The
|
// float4s per bone (column-vector affine [R | t], model -> world). The
|
||||||
// guest staging bank is reused draw to draw, hence the copy.
|
// guest staging bank is reused draw to draw, hence the copy.
|
||||||
|
|
@ -443,6 +448,15 @@ struct SubmitRecord {
|
||||||
// window (skate3_native_debug_dialog.cpp). The material bits are
|
// window (skate3_native_debug_dialog.cpp). The material bits are
|
||||||
// progressive looks (materials subsumes lighting subsumes albedo); the
|
// progressive looks (materials subsumes lighting subsumes albedo); the
|
||||||
// rest are independent and can reveal in any order or grouping.
|
// rest are independent and can reveal in any order or grouping.
|
||||||
|
// The decals and dyn layers are SUBTRACTIVE: their content is part of the
|
||||||
|
// normal frame and the layer hides it until revealed. When such a layer is
|
||||||
|
// left out of the order (or '-'disabled), the sequencer folds its bit into
|
||||||
|
// every step so the run matches the non-showcase frame from clay onward.
|
||||||
|
// The decal/grime composite only renders on the full material look, so the
|
||||||
|
// decals layer reads best placed after materials.
|
||||||
|
// Bit 1024 (blackout) is NOT an orderable layer: the sequencer brackets
|
||||||
|
// every run with it (snap to black at start, wipe to black at the end) as
|
||||||
|
// the recording cut marker.
|
||||||
struct ShowcaseLayer {
|
struct ShowcaseLayer {
|
||||||
const char* token;
|
const char* token;
|
||||||
uint32_t bit;
|
uint32_t bit;
|
||||||
|
|
@ -452,6 +466,8 @@ inline constexpr ShowcaseLayer kShowcaseLayers[] = {
|
||||||
{"albedo", 1u, "albedo textures"},
|
{"albedo", 1u, "albedo textures"},
|
||||||
{"lighting", 2u, "baked lighting"},
|
{"lighting", 2u, "baked lighting"},
|
||||||
{"materials", 4u, "materials & surface detail"},
|
{"materials", 4u, "materials & surface detail"},
|
||||||
|
{"decals", 256u, "decals & grime"},
|
||||||
|
{"dyn", 512u, "dynamic entities"},
|
||||||
{"shadows", 8u, "dynamic shadows"},
|
{"shadows", 8u, "dynamic shadows"},
|
||||||
{"ao", 16u, "ambient occlusion"},
|
{"ao", 16u, "ambient occlusion"},
|
||||||
{"ssr", 32u, "reflections"},
|
{"ssr", 32u, "reflections"},
|
||||||
|
|
@ -460,8 +476,10 @@ inline constexpr ShowcaseLayer kShowcaseLayers[] = {
|
||||||
};
|
};
|
||||||
inline constexpr size_t kShowcaseLayerCount =
|
inline constexpr size_t kShowcaseLayerCount =
|
||||||
sizeof(kShowcaseLayers) / sizeof(kShowcaseLayers[0]);
|
sizeof(kShowcaseLayers) / sizeof(kShowcaseLayers[0]);
|
||||||
|
// Bits of the subtractive layers (see above).
|
||||||
|
inline constexpr uint32_t kShowcaseSubtractiveMask = 256u | 512u;
|
||||||
inline constexpr const char* kShowcaseOrderDefault =
|
inline constexpr const char* kShowcaseOrderDefault =
|
||||||
"albedo,lighting,materials,shadows+ao,ssr,vol+bloom";
|
"albedo,lighting,materials,decals,dyn,shadows+ao,ssr,vol+bloom";
|
||||||
|
|
||||||
bool Enabled();
|
bool Enabled();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
// it through the RHI (D3D12 / Vulkan) into the guest output texture. Shared
|
// it through the RHI (D3D12 / Vulkan) into the guest output texture. Shared
|
||||||
// cross-thread state lives in skate3_native_scene_state.h.
|
// cross-thread state lives in skate3_native_scene_state.h.
|
||||||
|
|
||||||
|
#include "skate3_native_debug_dialog.h"
|
||||||
#include "skate3_native_scene.h"
|
#include "skate3_native_scene.h"
|
||||||
|
|
||||||
#include "generated/skate3_init.h"
|
#include "generated/skate3_init.h"
|
||||||
|
|
@ -69,6 +70,7 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_boot_native);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_cas_yield);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_cas_yield);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_char_shadow_exact);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_char_shadow_exact);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_decals);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_decals);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_dynamic_items);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_dynobj_v2);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_dynobj_v2);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_entity_fade);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_entity_fade);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_fmv_native);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_fmv_native);
|
||||||
|
|
@ -3282,9 +3284,17 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) {
|
||||||
g_r.world_shadow_in_srv = false;
|
g_r.world_shadow_in_srv = false;
|
||||||
g_r.world_shadow_primed = false;
|
g_r.world_shadow_primed = false;
|
||||||
}
|
}
|
||||||
const uint32_t want_static_size = uint32_t(std::clamp(
|
uint32_t want_static_size = uint32_t(std::clamp(
|
||||||
REXCVAR_GET(skate3_native_render_scene_shadow_static_size), 1024, 8192));
|
REXCVAR_GET(skate3_native_render_scene_shadow_static_size), 1024, 8192));
|
||||||
if (g_r.static_sun != nullptr && g_r.static_sun_size != want_static_size) {
|
if (device->backend() == nrhi::Backend::kD3D12) {
|
||||||
|
// The 3-tile cascade row must fit D3D12's 16384 2D-texture width cap:
|
||||||
|
// per-tile sizes above 5461 cannot allocate there (the 8192 setting
|
||||||
|
// asks for 24576x8192), while desktop Vulkan allows 32768-wide.
|
||||||
|
while (want_static_size * 3 > 16384) {
|
||||||
|
want_static_size /= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (g_r.static_sun != nullptr && g_r.static_sun_requested != want_static_size) {
|
||||||
// Hot size change: retire the map; recreated below. The recreated map
|
// Hot size change: retire the map; recreated below. The recreated map
|
||||||
// is uninitialized, so force the cross-frame cache to re-render it
|
// is uninitialized, so force the cross-frame cache to re-render it
|
||||||
// (RenderStaticSunMap rebuilds whenever nsm_built_radius <= 0).
|
// (RenderStaticSunMap rebuilds whenever nsm_built_radius <= 0).
|
||||||
|
|
@ -3302,6 +3312,7 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) {
|
||||||
// tiles side by side: inner (r/6, centimeter contact detail with
|
// tiles side by side: inner (r/6, centimeter contact detail with
|
||||||
// useful reach), mid (r/2) and far (full radius, large-caster
|
// useful reach), mid (r/2) and far (full radius, large-caster
|
||||||
// coverage); size is per tile.
|
// coverage); size is per tile.
|
||||||
|
g_r.static_sun_requested = want_static_size;
|
||||||
g_r.static_sun_size = want_static_size;
|
g_r.static_sun_size = want_static_size;
|
||||||
nrhi::TextureDesc desc;
|
nrhi::TextureDesc desc;
|
||||||
desc.width = g_r.static_sun_size * 3;
|
desc.width = g_r.static_sun_size * 3;
|
||||||
|
|
@ -3312,6 +3323,19 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) {
|
||||||
desc.clear_color[0] = 1.0f; // depth: far = lit
|
desc.clear_color[0] = 1.0f; // depth: far = lit
|
||||||
desc.clear_color[1] = 1.0f;
|
desc.clear_color[1] = 1.0f;
|
||||||
g_r.static_sun = device->CreateTexture(desc);
|
g_r.static_sun = device->CreateTexture(desc);
|
||||||
|
// Allocation-failure fallback (VRAM pressure, driver limits): a
|
||||||
|
// coarser static sun map beats marking the whole renderer failed and
|
||||||
|
// dropping the session to emulated output.
|
||||||
|
while (g_r.static_sun == nullptr && g_r.static_sun_size > 1024) {
|
||||||
|
g_r.static_sun_size /= 2;
|
||||||
|
desc.width = g_r.static_sun_size * 3;
|
||||||
|
desc.height = g_r.static_sun_size;
|
||||||
|
REXLOG_WARN(
|
||||||
|
"native-scene: static sun-shadow map allocation failed, retrying "
|
||||||
|
"at {}x{}",
|
||||||
|
g_r.static_sun_size * 3, g_r.static_sun_size);
|
||||||
|
g_r.static_sun = device->CreateTexture(desc);
|
||||||
|
}
|
||||||
if (g_r.static_sun != nullptr) {
|
if (g_r.static_sun != nullptr) {
|
||||||
nrhi::TextureViewDesc vd;
|
nrhi::TextureViewDesc vd;
|
||||||
vd.mip_levels = 1;
|
vd.mip_levels = 1;
|
||||||
|
|
@ -3571,6 +3595,23 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) {
|
||||||
g_r.targets_scene_fmt != want_scene_fmt ||
|
g_r.targets_scene_fmt != want_scene_fmt ||
|
||||||
g_r.targets_msaa != g_r.msaa) {
|
g_r.targets_msaa != g_r.msaa) {
|
||||||
if (g_r.depth) {
|
if (g_r.depth) {
|
||||||
|
// The AO/SSR/volumetric scene-depth SRVs alias this texture and
|
||||||
|
// re-point on pointer identity; heap reuse can hand the NEW depth
|
||||||
|
// texture the OLD one's address, so that comparison must never be
|
||||||
|
// the only invalidation. Retire the views with the buffer they view:
|
||||||
|
// a stale view kept sampling the retired depth image (still in
|
||||||
|
// attachment layout), a GPU fault and device hang on Vulkan.
|
||||||
|
nrhi::TextureView** depth_views[3] = {
|
||||||
|
&g_r.ao_depth_srv, &g_r.ssr_depth_srv, &g_r.vol_depth_srv};
|
||||||
|
nrhi::Texture** depth_views_of[3] = {
|
||||||
|
&g_r.ao_depth_srv_of, &g_r.ssr_depth_srv_of, &g_r.vol_depth_srv_of};
|
||||||
|
for (int v = 0; v < 3; ++v) {
|
||||||
|
if (*depth_views[v] != nullptr) {
|
||||||
|
g_r.device->DestroyDeferred(*depth_views[v]);
|
||||||
|
*depth_views[v] = nullptr;
|
||||||
|
}
|
||||||
|
*depth_views_of[v] = nullptr;
|
||||||
|
}
|
||||||
g_r.device->DestroyDeferred(g_r.depth);
|
g_r.device->DestroyDeferred(g_r.depth);
|
||||||
g_r.depth = nullptr;
|
g_r.depth = nullptr;
|
||||||
}
|
}
|
||||||
|
|
@ -6630,7 +6671,8 @@ static void TickShowcase(uint32_t output_width, bool hdr_on) {
|
||||||
}
|
}
|
||||||
// Layer availability: the material looks need nothing; the shadow and
|
// Layer availability: the material looks need nothing; the shadow and
|
||||||
// post layers join only when their feature is enabled (the post gates
|
// post layers join only when their feature is enabled (the post gates
|
||||||
// live in the HDR tonemap, so they need the HDR chain).
|
// live in the HDR tonemap, so they need the HDR chain). The subtractive
|
||||||
|
// layers need their content published/composited at all.
|
||||||
const auto layer_available = [&](uint32_t bit) -> bool {
|
const auto layer_available = [&](uint32_t bit) -> bool {
|
||||||
switch (bit) {
|
switch (bit) {
|
||||||
case 8u:
|
case 8u:
|
||||||
|
|
@ -6644,6 +6686,10 @@ static void TickShowcase(uint32_t output_width, bool hdr_on) {
|
||||||
REXCVAR_GET(skate3_native_render_scene_haze));
|
REXCVAR_GET(skate3_native_render_scene_haze));
|
||||||
case 128u:
|
case 128u:
|
||||||
return hdr_on && REXCVAR_GET(skate3_native_render_scene_bloom);
|
return hdr_on && REXCVAR_GET(skate3_native_render_scene_bloom);
|
||||||
|
case 256u:
|
||||||
|
return REXCVAR_GET(skate3_native_render_scene_decals);
|
||||||
|
case 512u:
|
||||||
|
return REXCVAR_GET(skate3_native_render_scene_dynamic_items);
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -6658,8 +6704,14 @@ static void TickShowcase(uint32_t output_width, bool hdr_on) {
|
||||||
// the skate3_native_render_scene_showcase_order cvar description):
|
// the skate3_native_render_scene_showcase_order cvar description):
|
||||||
// comma-separated steps, '+' joins layers into one wipe, '-' prefix =
|
// comma-separated steps, '+' joins layers into one wipe, '-' prefix =
|
||||||
// disabled. Unknown, unavailable and duplicate tokens drop out; steps
|
// disabled. Unknown, unavailable and duplicate tokens drop out; steps
|
||||||
// left empty are skipped. The run always starts from clay.
|
// left empty are skipped. The run opens ON black (mask bit 1024; a
|
||||||
|
// snap, not a wipe - see the step-0 timing below), wipes clay in from
|
||||||
|
// it, and closes by wiping back out to black. The >= hold_s stretches
|
||||||
|
// of pure black double as machine-findable cut markers for screen
|
||||||
|
// recordings (local/scripts/cut_showcase_clips.py pairs them with
|
||||||
|
// ffmpeg blackdetect).
|
||||||
s.steps.clear();
|
s.steps.clear();
|
||||||
|
s.steps.push_back({float(256u + 1024u), "black (start marker)"});
|
||||||
s.steps.push_back({256.0f, "clay geometry"});
|
s.steps.push_back({256.0f, "clay geometry"});
|
||||||
uint32_t cum = 0;
|
uint32_t cum = 0;
|
||||||
const std::string order =
|
const std::string order =
|
||||||
|
|
@ -6715,14 +6767,28 @@ static void TickShowcase(uint32_t output_width, bool hdr_on) {
|
||||||
s.steps.push_back({float(256u + cum), std::move(label)});
|
s.steps.push_back({float(256u + cum), std::move(label)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Subtractive layers (decals/grime, dynamic entities) left out of the
|
||||||
|
// order or '-'disabled are not build-up beats: fold their bits into
|
||||||
|
// every step, clay included, so their content renders from the first
|
||||||
|
// frame exactly as in the non-showcase frame. An UNAVAILABLE layer
|
||||||
|
// needs no fold; its feature cvar already removes the content itself.
|
||||||
|
const uint32_t always = kShowcaseSubtractiveMask & avail_mask & ~cum;
|
||||||
|
if (always != 0u) {
|
||||||
|
for (Step& st : s.steps) {
|
||||||
|
st.value = float(256u + ((uint32_t(st.value) - 256u) | always));
|
||||||
|
}
|
||||||
|
}
|
||||||
// A final full-render reveal when the list leaves layers unrevealed
|
// A final full-render reveal when the list leaves layers unrevealed
|
||||||
// (the materials look subsumes the albedo/lighting bits, so those two
|
// (the materials look subsumes the albedo/lighting bits, so those two
|
||||||
// stop counting once materials is in).
|
// stop counting once materials is in; the subtractive bits are either
|
||||||
|
// revealed steps or folded in above, so they never force the step).
|
||||||
const bool covered =
|
const bool covered =
|
||||||
(cum & 4u) != 0 && ((avail_mask & ~cum) & 0xF8u) == 0u;
|
(cum & 4u) != 0 && ((avail_mask & ~cum) & 0xF8u) == 0u;
|
||||||
if (!covered) {
|
if (!covered) {
|
||||||
s.steps.push_back({0.0f, "full render"});
|
s.steps.push_back({0.0f, "full render"});
|
||||||
}
|
}
|
||||||
|
// Closing blackout bookend (the end-of-run cut marker).
|
||||||
|
s.steps.push_back({float(256u + 1024u), "black (end marker)"});
|
||||||
// Durations snapshot at start so a live cvar edit cannot jump the
|
// Durations snapshot at start so a live cvar edit cannot jump the
|
||||||
// elapsed-time -> step mapping mid-run.
|
// elapsed-time -> step mapping mid-run.
|
||||||
s.wipe_s = std::max(0.2, REXCVAR_GET(skate3_native_render_scene_showcase_wipe));
|
s.wipe_s = std::max(0.2, REXCVAR_GET(skate3_native_render_scene_showcase_wipe));
|
||||||
|
|
@ -6743,7 +6809,20 @@ static void TickShowcase(uint32_t output_width, bool hdr_on) {
|
||||||
const double elapsed =
|
const double elapsed =
|
||||||
std::chrono::duration<double>(PerfClock::now() - s.t0).count();
|
std::chrono::duration<double>(PerfClock::now() - s.t0).count();
|
||||||
const double per_step = s.wipe_s + s.hold_s;
|
const double per_step = s.wipe_s + s.hold_s;
|
||||||
const int idx = int(elapsed / per_step);
|
// Step 0 (the blackout start bookend) SNAPS to black instead of wiping
|
||||||
|
// from the live frame and lasts hold_s only: the recording cut lands on
|
||||||
|
// a hard black edge, and the build-up proper starts with clay wiping in
|
||||||
|
// from black.
|
||||||
|
int idx;
|
||||||
|
double phase;
|
||||||
|
if (elapsed < s.hold_s) {
|
||||||
|
idx = 0;
|
||||||
|
phase = s.wipe_s; // past the wipe window: hold, no split
|
||||||
|
} else {
|
||||||
|
const double e = elapsed - s.hold_s;
|
||||||
|
idx = 1 + int(e / per_step);
|
||||||
|
phase = e - double(idx - 1) * per_step;
|
||||||
|
}
|
||||||
if (idx >= int(s.steps.size())) {
|
if (idx >= int(s.steps.size())) {
|
||||||
s.active = false;
|
s.active = false;
|
||||||
REXCVAR_SET(skate3_native_render_scene_showcase, false);
|
REXCVAR_SET(skate3_native_render_scene_showcase, false);
|
||||||
|
|
@ -6757,7 +6836,6 @@ static void TickShowcase(uint32_t output_width, bool hdr_on) {
|
||||||
s.logged = idx;
|
s.logged = idx;
|
||||||
}
|
}
|
||||||
const float prev = idx == 0 ? 0.0f : s.steps[idx - 1].value;
|
const float prev = idx == 0 ? 0.0f : s.steps[idx - 1].value;
|
||||||
const double phase = elapsed - double(idx) * per_step;
|
|
||||||
if (phase < s.wipe_s) {
|
if (phase < s.wipe_s) {
|
||||||
float f = float(phase / s.wipe_s);
|
float f = float(phase / s.wipe_s);
|
||||||
f = f * f * (3.0f - 2.0f * f); // ease the sweep in and out
|
f = f * f * (3.0f - 2.0f * f); // ease the sweep in and out
|
||||||
|
|
@ -6777,6 +6855,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
context.backend != NativeGuestOutputBackend::kVulkan)) {
|
context.backend != NativeGuestOutputBackend::kVulkan)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
// Debug stress driver (skate3_native_render_scene_maxq_cycle): cycles
|
||||||
|
// the max-quality toggle to exercise the hot rebuild path.
|
||||||
|
static uint64_t s_maxq_cycle_frame = 0;
|
||||||
|
skate3::MaxQualityAutoCycle(++s_maxq_cycle_frame);
|
||||||
|
}
|
||||||
// While the game reports menus / loading (presence context 0x8001 == 0),
|
// While the game reports menus / loading (presence context 0x8001 == 0),
|
||||||
// yield to the emulated output, EXCEPT the in-game pause menu (world
|
// yield to the emulated output, EXCEPT the in-game pause menu (world
|
||||||
// still publishing perspective scenes), which stays native when
|
// still publishing perspective scenes), which stays native when
|
||||||
|
|
@ -8036,7 +8120,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// alpha-clipped (their VS skinning branch is inert: zero weights).
|
// alpha-clipped (their VS skinning branch is inert: zero weights).
|
||||||
constants[33] = (bones_bound || item.ropa) ? 1.0f : 0.0f;
|
constants[33] = (bones_bound || item.ropa) ? 1.0f : 0.0f;
|
||||||
constants[34] = item.unlit ? 1.0f : 0.0f;
|
constants[34] = item.unlit ? 1.0f : 0.0f;
|
||||||
constants[35] = 0.0f;
|
// tint.a < 0 marks dynamic-entity items for the showcase dyn layer
|
||||||
|
// (ps_main clips them until the bit reveals); the solid-color
|
||||||
|
// early-out only ever reads tint.a > 0.
|
||||||
|
constants[35] = item.dyn_entity ? -1.0f : 0.0f;
|
||||||
}
|
}
|
||||||
constants[36] = scene.cam_pos[0];
|
constants[36] = scene.cam_pos[0];
|
||||||
constants[37] = scene.cam_pos[1];
|
constants[37] = scene.cam_pos[1];
|
||||||
|
|
@ -9362,6 +9449,17 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
float spline_consts[20];
|
float spline_consts[20];
|
||||||
std::memcpy(spline_consts, scene.view_proj, sizeof(float) * 16);
|
std::memcpy(spline_consts, scene.view_proj, sizeof(float) * 16);
|
||||||
std::memcpy(spline_consts + 16, s.consts + 149 * 4, sizeof(float) * 4);
|
std::memcpy(spline_consts + 16, s.consts + 149 * 4, sizeof(float) * 4);
|
||||||
|
// intensity.zw = showcase blackout gate (the guest row's unused
|
||||||
|
// lanes): z = split x, w = left|right visibility bits. The spline PS
|
||||||
|
// draws outside the scene shaders, so the blackout stage must gate
|
||||||
|
// it here or the neon lines leak over the black recording bookends.
|
||||||
|
const auto side_visible = [](float v) {
|
||||||
|
return v < 255.5f || ((uint32_t(v + 0.5f) - 256u) & 1024u) == 0u;
|
||||||
|
};
|
||||||
|
spline_consts[18] = g_r.showcase_rows[2];
|
||||||
|
spline_consts[19] =
|
||||||
|
float((side_visible(g_r.showcase_rows[0]) ? 1 : 0) |
|
||||||
|
(side_visible(g_r.showcase_rows[1]) ? 2 : 0));
|
||||||
cmd->SetRootConstants(0, 20, spline_consts, 0);
|
cmd->SetRootConstants(0, 20, spline_consts, 0);
|
||||||
cmd->SetTexture(1, spline_tex->srv);
|
cmd->SetTexture(1, spline_tex->srv);
|
||||||
cmd->SetVertexBuffer(g_r.ui_ring, ui_region + ui_offset, bytes, 28);
|
cmd->SetVertexBuffer(g_r.ui_ring, ui_region + ui_offset, bytes, 28);
|
||||||
|
|
|
||||||
|
|
@ -840,6 +840,11 @@ struct RendererState {
|
||||||
bool static_sun_in_srv = false;
|
bool static_sun_in_srv = false;
|
||||||
bool static_sun_valid = false;
|
bool static_sun_valid = false;
|
||||||
uint32_t static_sun_size = 0;
|
uint32_t static_sun_size = 0;
|
||||||
|
// The per-tile size this map was requested at (post backend-limit clamp,
|
||||||
|
// pre allocation-failure fallback). The hot-size-change retire keys on
|
||||||
|
// this, not static_sun_size: a map that allocated smaller than requested
|
||||||
|
// must not be retired and re-tried every frame.
|
||||||
|
uint32_t static_sun_requested = 0;
|
||||||
float nsm_rows[12] = {};
|
float nsm_rows[12] = {};
|
||||||
float nsm_depth_range = 1.0f; // meters per depth-map unit
|
float nsm_depth_range = 1.0f; // meters per depth-map unit
|
||||||
float nsm_radius = 1.0f; // far-tile ortho half-extent in meters
|
float nsm_radius = 1.0f; // far-tile ortho half-extent in meters
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue