flag/banner flicker: cull the double-sided sheet twin + generate lightmap mips

This commit is contained in:
mchughalex 2026-07-07 03:03:40 +01:00
parent acd77da02a
commit ab512f5620

View file

@ -2461,6 +2461,14 @@ struct MeshBuffers {
D3D12_VERTEX_BUFFER_VIEW vb_view{}; D3D12_VERTEX_BUFFER_VIEW vb_view{};
D3D12_INDEX_BUFFER_VIEW ib_view{}; D3D12_INDEX_BUFFER_VIEW ib_view{};
uint64_t fingerprint = 0; uint64_t fingerprint = 0;
// Double-sided sheet prop (banners/flags): most triangles have an
// opposite-winding twin ~1cm behind, and the two faces map to DIFFERENT
// lightmap atlas cells (lit vs shaded side). Drawn without culling both
// faces z-fight once their depth gap drops below buffer precision;
// distant triangles alternate between the two cells per frame (the "flag
// flicker", stops up close where 1cm still resolves). These meshes draw
// with the backface-culling PSO instead.
bool two_sided_sheet = false;
}; };
struct GuestTexture { struct GuestTexture {
@ -2592,6 +2600,7 @@ struct RendererState {
ID3D12Device* device = nullptr; ID3D12Device* device = nullptr;
ID3D12RootSignature* root_signature = nullptr; ID3D12RootSignature* root_signature = nullptr;
ID3D12PipelineState* pso = nullptr; ID3D12PipelineState* pso = nullptr;
ID3D12PipelineState* pso_cullback = nullptr; // two_sided_sheet meshes (see MeshBuffers)
ID3D12PipelineState* pso_nodepth = nullptr; ID3D12PipelineState* pso_nodepth = nullptr;
// environment.transparent sub-pass: straight alpha blend, depth test on, // environment.transparent sub-pass: straight alpha blend, depth test on,
// z-write OFF; items drawn back-to-front after all opaque items. // z-write OFF; items drawn back-to-front after all opaque items.
@ -3129,6 +3138,21 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item,
return false; return false;
} }
// two_sided_sheet detection eligibility (see MeshBuffers): small static
// triangle-list meshes only. Strips alternate winding per triangle and
// skinned/cloth meshes deform, so both stay on the uncull(ed) PSO.
bool detect_sheet =
!item.skinned && !item.cloth_quads && item.ib_count >= 6 && item.ib_count <= 8192;
for (const DrawEntry& de : item.draws) {
if (de.prim != 4) {
detect_sheet = false;
}
}
std::vector<float> sheet_pos;
if (detect_sheet) {
sheet_pos.reserve(size_t(num_verts) * 3);
}
const uint8_t* src_vb = vb_scratch.data(); const uint8_t* src_vb = vb_scratch.data();
float* dst = nullptr; float* dst = nullptr;
uint32_t garbage = 0; uint32_t garbage = 0;
@ -3209,6 +3233,11 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item,
dst[v * 14 + 0] = x; dst[v * 14 + 0] = x;
dst[v * 14 + 1] = y; dst[v * 14 + 1] = y;
dst[v * 14 + 2] = z; dst[v * 14 + 2] = z;
if (detect_sheet) {
sheet_pos.push_back(x);
sheet_pos.push_back(y);
sheet_pos.push_back(z);
}
decode_uv(item.uv_fmt, item.uv_offset, dst[v * 14 + 3], dst[v * 14 + 4]); decode_uv(item.uv_fmt, item.uv_offset, dst[v * 14 + 3], dst[v * 14 + 4]);
decode_uv(item.uv2_fmt, item.uv2_offset, dst[v * 14 + 5], dst[v * 14 + 6]); decode_uv(item.uv2_fmt, item.uv2_offset, dst[v * 14 + 5], dst[v * 14 + 6]);
// The second texcoord is the lightmap/decal UNWRAP, stored as an // The second texcoord is the lightmap/decal UNWRAP, stored as an
@ -3307,12 +3336,109 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item,
dst_ib[i] = SwapU16(src_ib[i]); dst_ib[i] = SwapU16(src_ib[i]);
} }
} }
// Front/back sheet pattern: opposite-winding twin triangles a few mm-cm
// apart ALONG THE NORMAL. The two sides are often triangulated along
// OPPOSITE quad diagonals (downtown lamppost posters: sheets 4mm apart,
// twin centroids 0.26m apart in-plane), so twins are matched by
// plane-to-plane distance with an in-plane tolerance scaled to triangle
// size; raw centroid distance misses them. >=60% twinned marks the mesh
// double-sided (banners are 100%); O(T^2) but only for <=8192-index
// static tri-list meshes and only once per decode.
bool two_sided = false;
if (detect_sheet) {
struct SheetTri {
float c[3];
float n[3];
float edge; // longest edge length (in-plane tolerance scale)
};
std::vector<SheetTri> tris;
tris.reserve(item.ib_count / 3);
for (const DrawEntry& de : item.draws) {
if (uint64_t(de.start_index) + de.index_count > item.ib_count) {
continue;
}
for (uint32_t i = 0; i + 2 < de.index_count; i += 3) {
const uint32_t a = uint32_t(dst_ib[de.start_index + i]) + de.base_vertex;
const uint32_t b = uint32_t(dst_ib[de.start_index + i + 1]) + de.base_vertex;
const uint32_t c = uint32_t(dst_ib[de.start_index + i + 2]) + de.base_vertex;
if (a >= num_verts || b >= num_verts || c >= num_verts) {
continue;
}
const float* pa = &sheet_pos[size_t(a) * 3];
const float* pb = &sheet_pos[size_t(b) * 3];
const float* pc = &sheet_pos[size_t(c) * 3];
const float e1[3] = {pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]};
const float e2[3] = {pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]};
const float n[3] = {e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2],
e1[0] * e2[1] - e1[1] * e2[0]};
const float len = std::sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
if (len < 1e-8f) {
continue;
}
SheetTri t;
for (int k = 0; k < 3; ++k) {
t.c[k] = (pa[k] + pb[k] + pc[k]) / 3.0f;
t.n[k] = n[k] / len;
}
const float e3[3] = {pc[0] - pb[0], pc[1] - pb[1], pc[2] - pb[2]};
const float l1 = e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2];
const float l2 = e2[0] * e2[0] + e2[1] * e2[1] + e2[2] * e2[2];
const float l3 = e3[0] * e3[0] + e3[1] * e3[1] + e3[2] * e3[2];
t.edge = std::sqrt(std::max(l1, std::max(l2, l3)));
tris.push_back(t);
}
}
if (tris.size() >= 2) {
std::vector<char> used(tris.size(), 0);
size_t twins = 0;
for (size_t i = 0; i < tris.size(); ++i) {
if (used[i]) {
continue;
}
for (size_t j = i + 1; j < tris.size(); ++j) {
if (used[j]) {
continue;
}
const float dx = tris[i].c[0] - tris[j].c[0];
const float dy = tris[i].c[1] - tris[j].c[1];
const float dz = tris[i].c[2] - tris[j].c[2];
const float d2 = dx * dx + dy * dy + dz * dz;
// Plane separation along i's normal must be small (back-to-back
// sheets); in-plane offset up to the triangle scale (opposite
// diagonal splits put twin centroids half a quad apart).
const float along = std::fabs(dx * tris[i].n[0] + dy * tris[i].n[1] +
dz * tris[i].n[2]);
const float lat_limit = 0.5f * (tris[i].edge + tris[j].edge);
if (along > 0.05f || d2 - along * along > lat_limit * lat_limit) {
continue;
}
const float dot = tris[i].n[0] * tris[j].n[0] + tris[i].n[1] * tris[j].n[1] +
tris[i].n[2] * tris[j].n[2];
if (dot < -0.9f) {
used[i] = 1;
used[j] = 1;
twins += 2;
break;
}
}
}
two_sided = twins * 10 >= tris.size() * 6;
if (two_sided) {
static std::atomic<uint32_t> logged{0};
if (logged.fetch_add(1, std::memory_order_relaxed) < 16) {
REXLOG_INFO("native-scene: two-sided sheet mesh {:08X} ({} indices, {}/{} twins) -> cull-back",
item.mesh, item.ib_count, twins, tris.size());
}
}
}
}
ib->Unmap(0, nullptr); ib->Unmap(0, nullptr);
out.vb = vb; out.vb = vb;
out.ib = ib; out.ib = ib;
out.vb_view = {vb->GetGPUVirtualAddress(), num_verts * 56u, 56u}; out.vb_view = {vb->GetGPUVirtualAddress(), num_verts * 56u, 56u};
out.ib_view = {ib->GetGPUVirtualAddress(), item.ib_count * 2u, DXGI_FORMAT_R16_UINT}; out.ib_view = {ib->GetGPUVirtualAddress(), item.ib_count * 2u, DXGI_FORMAT_R16_UINT};
out.two_sided_sheet = two_sided;
return true; return true;
} }
@ -3321,6 +3447,259 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item,
// physical mirror, endian swap, and create its SRV in the staging heap. // physical mirror, endian swap, and create its SRV in the staging heap.
// The 3D path reads the words from renderengine::Texture objects; the 2D // The 3D path reads the words from renderengine::Texture objects; the 2D
// path passes the device fetch-shadow words directly. // path passes the device fetch-shadow words directly.
// BC1/DXT1 block decode (both color modes) into 16 RGBA8 texels.
void DecodeBc1Block(const uint8_t* b, uint8_t px[16][4]) {
const uint16_t c0 = uint16_t(b[0] | (b[1] << 8));
const uint16_t c1 = uint16_t(b[2] | (b[3] << 8));
uint8_t col[4][4];
const auto expand = [](uint16_t c, uint8_t* o) {
o[0] = uint8_t(((c >> 11) & 31) * 255 / 31);
o[1] = uint8_t(((c >> 5) & 63) * 255 / 63);
o[2] = uint8_t((c & 31) * 255 / 31);
o[3] = 255;
};
expand(c0, col[0]);
expand(c1, col[1]);
if (c0 > c1) {
for (int k = 0; k < 3; ++k) {
col[2][k] = uint8_t((2 * col[0][k] + col[1][k]) / 3);
col[3][k] = uint8_t((col[0][k] + 2 * col[1][k]) / 3);
}
col[2][3] = 255;
col[3][3] = 255;
} else {
for (int k = 0; k < 3; ++k) {
col[2][k] = uint8_t((col[0][k] + col[1][k]) / 2);
col[3][k] = 0;
}
col[2][3] = 255;
col[3][3] = 0;
}
uint32_t bits =
uint32_t(b[4]) | (uint32_t(b[5]) << 8) | (uint32_t(b[6]) << 16) | (uint32_t(b[7]) << 24);
for (int i = 0; i < 16; ++i) {
std::memcpy(px[i], col[bits & 3u], 4);
bits >>= 2;
}
}
// Runtime-composed textures (lightmap atlas pages above all) ship with NO
// mip chain; sampling their razor-contrast mip 0 under minification
// shimmers, grazing-angle banner posters, distant thin geometry, because
// the pixel footprint spans many texels the aniso sampler cannot average at
// mip 0 alone. The game masked this with its 720p softness and the 4-tap
// lightmap filter; at native 4K we need real prefiltering. Small no-mip
// DXT1/8888 textures are decoded to RGBA8 and uploaded with a CPU
// box-filtered mip chain instead. Returns false to fall back to the plain
// single-mip path.
bool UploadGeneratedMips(const NativeGuestOutputRenderContext& context, uint8_t* base,
const rex::graphics::TextureInfo& info, uint32_t fetch_swizzle,
GuestTexture& out) {
const rex::graphics::FormatInfo* format_info = info.format_info();
const uint32_t bytes_per_block = format_info->bytes_per_block();
const uint32_t bytes_per_block_log2 = uint32_t(std::countr_zero(bytes_per_block));
const uint32_t width = info.width + 1u;
const uint32_t height = info.height + 1u;
const uint32_t block_w = format_info->block_width;
const uint32_t block_h = format_info->block_height;
const bool bc1 =
rex::graphics::GetBaseFormat(info.format) == xenos::TextureFormat::k_DXT1;
// Guest mip 0 copy: same packed-base + tiled macro-row padding rules as
// the plain path.
uint32_t ox = 0, oy = 0;
const uint32_t addr = info.GetMipLocation(0, &ox, &oy, true);
if (addr == 0) {
return false;
}
const uint32_t pitch_blocks = info.extent.block_pitch_h;
const uint32_t min_size = info.memory.base_size;
uint32_t size = min_size;
const uint32_t cols = (width + block_w - 1) / block_w;
const uint32_t rows = (height + block_h - 1) / block_h;
if (info.is_tiled) {
const uint32_t padded_rows = ((rows + oy) + 31u) & ~31u;
size = std::max(size, padded_rows * pitch_blocks * bytes_per_block);
}
static thread_local std::vector<uint8_t> gen_scratch;
gen_scratch.resize(size);
if (!GuestTryCopy(gen_scratch.data(), base + (0xA0000000u | addr), size)) {
if (min_size >= size ||
!GuestTryCopy(gen_scratch.data(), base + (0xA0000000u | addr), min_size)) {
return false;
}
size = min_size;
}
// Untile into linear block rows, endian-swap per row.
std::vector<uint8_t> linear(size_t(cols) * rows * bytes_per_block);
for (uint32_t by = 0; by < rows; ++by) {
uint8_t* out_row = linear.data() + size_t(by) * cols * bytes_per_block;
for (uint32_t bx = 0; bx < cols; ++bx) {
uint32_t source_offset;
if (info.is_tiled) {
source_offset = uint32_t(rex::graphics::texture_util::GetTiledOffset2D(
int32_t(bx + ox), int32_t(by + oy), pitch_blocks, bytes_per_block_log2));
} else {
source_offset = ((by + oy) * pitch_blocks + bx + ox) * bytes_per_block;
}
if (source_offset + bytes_per_block > size) {
std::memset(out_row + size_t(bx) * bytes_per_block, 0, bytes_per_block);
continue;
}
std::memcpy(out_row + size_t(bx) * bytes_per_block, gen_scratch.data() + source_offset,
bytes_per_block);
}
SwapGuestEndian(out_row, cols * bytes_per_block, info.endianness);
}
// Decode to RGBA8 mip 0.
std::vector<uint8_t> rgba(size_t(width) * height * 4);
if (bc1) {
for (uint32_t by = 0; by < rows; ++by) {
for (uint32_t bx = 0; bx < cols; ++bx) {
uint8_t px[16][4];
DecodeBc1Block(linear.data() + (size_t(by) * cols + bx) * bytes_per_block, px);
for (uint32_t t = 0; t < 16; ++t) {
const uint32_t x = bx * 4 + (t & 3u);
const uint32_t y = by * 4 + (t >> 2);
if (x < width && y < height) {
std::memcpy(&rgba[(size_t(y) * width + x) * 4], px[t], 4);
}
}
}
}
} else { // k_8_8_8_8: rows are already RGBA8 after the endian swap
for (uint32_t y = 0; y < height; ++y) {
std::memcpy(&rgba[size_t(y) * width * 4], linear.data() + size_t(y) * cols * 4,
size_t(width) * 4);
}
}
// Box-filtered chain down to 1x1.
std::vector<std::vector<uint8_t>> mips;
mips.emplace_back(std::move(rgba));
uint32_t mw = width, mh = height;
while (mw > 1 || mh > 1) {
const uint32_t nw = std::max(mw >> 1, 1u);
const uint32_t nh = std::max(mh >> 1, 1u);
const std::vector<uint8_t>& srcm = mips.back();
std::vector<uint8_t> dstm(size_t(nw) * nh * 4);
for (uint32_t y = 0; y < nh; ++y) {
const uint32_t y0 = std::min(y * 2, mh - 1);
const uint32_t y1 = std::min(y * 2 + 1, mh - 1);
for (uint32_t x = 0; x < nw; ++x) {
const uint32_t x0 = std::min(x * 2, mw - 1);
const uint32_t x1 = std::min(x * 2 + 1, mw - 1);
for (int k = 0; k < 4; ++k) {
const uint32_t sum = srcm[(size_t(y0) * mw + x0) * 4 + k] +
srcm[(size_t(y0) * mw + x1) * 4 + k] +
srcm[(size_t(y1) * mw + x0) * 4 + k] +
srcm[(size_t(y1) * mw + x1) * 4 + k];
dstm[(size_t(y) * nw + x) * 4 + k] = uint8_t((sum + 2) / 4);
}
}
}
mips.emplace_back(std::move(dstm));
mw = nw;
mh = nh;
}
const uint32_t mip_count = uint32_t(mips.size());
// Upload plan + resource (RGBA8).
struct Plan {
uint32_t offset, pitch, w, h;
};
std::vector<Plan> plans(mip_count);
uint32_t upload_size = 0;
for (uint32_t m = 0; m < mip_count; ++m) {
Plan& p = plans[m];
p.w = std::max(width >> m, 1u);
p.h = std::max(height >> m, 1u);
p.pitch = (p.w * 4u + (D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u)) &
~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u);
p.offset = (upload_size + (D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT - 1u)) &
~(D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT - 1u);
upload_size = p.offset + p.pitch * p.h;
}
ID3D12Device* device = context.d3d12.device;
D3D12_HEAP_PROPERTIES heap{};
heap.Type = D3D12_HEAP_TYPE_DEFAULT;
D3D12_RESOURCE_DESC desc{};
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.Width = width;
desc.Height = height;
desc.DepthOrArraySize = 1;
desc.MipLevels = UINT16(mip_count);
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc,
D3D12_RESOURCE_STATE_COPY_DEST, nullptr,
IID_PPV_ARGS(&out.texture)))) {
out.texture = nullptr;
return false;
}
out.upload = CreateUploadBuffer(device, upload_size);
if (!out.upload) {
out.texture->Release();
out.texture = nullptr;
return false;
}
uint8_t* mapping = nullptr;
out.upload->Map(0, nullptr, reinterpret_cast<void**>(&mapping));
for (uint32_t m = 0; m < mip_count; ++m) {
const Plan& p = plans[m];
for (uint32_t y = 0; y < p.h; ++y) {
std::memcpy(mapping + p.offset + size_t(y) * p.pitch, &mips[m][size_t(y) * p.w * 4],
size_t(p.w) * 4);
}
}
out.upload->Unmap(0, nullptr);
auto* command_processor = context.d3d12.command_processor;
auto& list = command_processor->GetDeferredCommandList();
for (uint32_t m = 0; m < mip_count; ++m) {
const Plan& p = plans[m];
D3D12_TEXTURE_COPY_LOCATION dst{};
dst.pResource = out.texture;
dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
dst.SubresourceIndex = m;
D3D12_TEXTURE_COPY_LOCATION src{};
src.pResource = out.upload;
src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
src.PlacedFootprint.Offset = p.offset;
src.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
src.PlacedFootprint.Footprint.Width = p.w;
src.PlacedFootprint.Footprint.Height = p.h;
src.PlacedFootprint.Footprint.Depth = 1;
src.PlacedFootprint.Footprint.RowPitch = p.pitch;
list.D3DCopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
}
context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data,
out.texture, D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
if (g_r.srv_next >= 8192) {
return false;
}
out.srv_slot = g_r.srv_next++;
D3D12_SHADER_RESOURCE_VIEW_DESC srv{};
srv.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srv.Shader4ComponentMapping =
ComposeSrvSwizzle(fetch_swizzle, xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA);
srv.Texture2D.MipLevels = mip_count;
D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart();
slot.ptr += size_t(out.srv_slot) * g_r.srv_size;
device->CreateShaderResourceView(out.texture, &srv, slot);
out.payload_addr = 0xA0000000u | info.memory.base_address;
out.payload_size = size;
out.payload_fp = SamplePayloadFingerprint(base, out.payload_addr, out.payload_size);
out.recheck_frame = 0;
out.valid = true;
return true;
}
bool EnsureGuestTextureFromWords(const NativeGuestOutputRenderContext& context, bool EnsureGuestTextureFromWords(const NativeGuestOutputRenderContext& context,
uint8_t* base, const uint32_t words[6], uint8_t* base, const uint32_t words[6],
GuestTexture& out) { GuestTexture& out) {
@ -3380,6 +3759,21 @@ bool EnsureGuestTextureFromWords(const NativeGuestOutputRenderContext& context,
++mip_count; ++mip_count;
} }
} }
// No guest chain at all (runtime-composed lightmap pages): generate one,
// see UploadGeneratedMips. Small DXT1/8888 textures only; falls back to
// the plain single-mip path on any failure.
if (mip_count == 1 && pow2 && REXCVAR_GET(skate3_native_render_scene_tex_mips) &&
width >= 8 && height >= 8 && width <= 512 && height <= 512) {
const auto base_fmt = rex::graphics::GetBaseFormat(info.format);
if (base_fmt == xenos::TextureFormat::k_DXT1 ||
base_fmt == xenos::TextureFormat::k_8_8_8_8) {
GuestTexture gen = out; // keeps fetch_words already copied
if (UploadGeneratedMips(context, base, info, fetch.swizzle, gen)) {
out = gen;
return true;
}
}
}
// Copy the whole guest mip chain out up front with the lock-free guarded // Copy the whole guest mip chain out up front with the lock-free guarded
// copy (never VirtualQuery on the render thread: the VAD lock stalls // copy (never VirtualQuery on the render thread: the VAD lock stalls
@ -3735,6 +4129,22 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) {
pso.DSVFormat = DXGI_FORMAT_D32_FLOAT; pso.DSVFormat = DXGI_FORMAT_D32_FLOAT;
pso.SampleDesc.Count = g_r.msaa; pso.SampleDesc.Count = g_r.msaa;
const HRESULT hr = device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&g_r.pso)); const HRESULT hr = device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&g_r.pso));
// Culling variant for double-sided sheet props (banners/flags,
// MeshBuffers::two_sided_sheet). The sheet whose winding-derived world
// normal faces the camera is the one the game keeps (its lightmap cell
// reproduces the emulated banner exactly: albedo x lm x 2 lands within
// 3-8% of the F11 emulated reference on capture 1783387480); under our
// pipeline those triangles are D3D12 BACK faces, so cull FRONT.
// (CULL_BACK was tried first and kept the wrong sheet; banners rendered
// the sun-side lightmap cell ~2.4x brighter than emulated.)
pso.RasterizerState.CullMode = D3D12_CULL_MODE_FRONT;
const HRESULT hr_cb =
device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&g_r.pso_cullback));
if (FAILED(hr_cb)) {
REXLOG_WARN("native-scene: cull-back PSO creation failed {:08X}", uint32_t(hr_cb));
g_r.pso_cullback = nullptr; // sheets fall back to the uncull(ed) PSO
}
pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
// Transparent variant: straight alpha blend, depth-tested, no z-write. // Transparent variant: straight alpha blend, depth-tested, no z-write.
pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO; pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
pso.BlendState.RenderTarget[0].BlendEnable = TRUE; pso.BlendState.RenderTarget[0].BlendEnable = TRUE;
@ -4587,6 +4997,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
list.RSSetScissorRect(scissor); list.RSSetScissorRect(scissor);
list.D3DSetGraphicsRootSignature(g_r.root_signature); list.D3DSetGraphicsRootSignature(g_r.root_signature);
list.D3DSetPipelineState(use_depth ? g_r.pso : g_r.pso_nodepth); list.D3DSetPipelineState(use_depth ? g_r.pso : g_r.pso_nodepth);
// Per-item PSO tracking for the opaque pass: two_sided_sheet meshes swap
// to the backface-culling variant (see MeshBuffers), everything else uses
// the pass PSO. Only meaningful while use_depth; the transparent sub-pass
// sets its own PSO and is not switched per item.
ID3D12PipelineState* scene_pso_bound = use_depth ? g_r.pso : g_r.pso_nodepth;
// Our own persistent shader-visible SRV heap for the whole pass. These are // Our own persistent shader-visible SRV heap for the whole pass. These are
// the last commands of the submission, so displacing the emulated GPU's // the last commands of the submission, so displacing the emulated GPU's
// heap binding is safe. // heap binding is safe.
@ -4670,6 +5085,23 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
} }
const MeshBuffers& buffers = it->second; const MeshBuffers& buffers = it->second;
// Double-sided sheet props draw with backface culling; without it the
// front/back copies z-fight into lightmap flicker at range (banners/
// flags). Opaque depth pass only; a mirrored instance (negative world
// determinant) would flip winding, so those stay uncull(ed).
if (use_depth && !item.transparent && g_r.pso_cullback != nullptr) {
const float* w = item.world;
const float det3 = w[0] * (w[5] * w[10] - w[6] * w[9]) -
w[1] * (w[4] * w[10] - w[6] * w[8]) +
w[2] * (w[4] * w[9] - w[5] * w[8]);
ID3D12PipelineState* want =
(buffers.two_sided_sheet && det3 >= 0.0f) ? g_r.pso_cullback : g_r.pso;
if (want != scene_pso_bound) {
list.D3DSetPipelineState(want);
scene_pso_bound = want;
}
}
// Resolve guest textures (white fallback). Cached decodes revalidate // Resolve guest textures (white fallback). Cached decodes revalidate
// against the live fetch words; streaming reuses texture objects. The // against the live fetch words; streaming reuses texture objects. The
// object addresses were readable on the game thread this frame; NO // object addresses were readable on the game thread this frame; NO