using System.Numerics;
using AcDream.App.Rendering.Gpu;
namespace AcDream.App.Rendering;
///
/// Testable CPU statement of retail's detail-pass gate and pixel math. The
/// production pixels are produced by mesh_detail; keeping these facts
/// in one small contract makes the setting and the combine independently
/// assertable without a GPU.
///
/// VM2 (2026-08-22, live cdb read on the PDB-paired retail client,
/// docs/research/2026-08-22-vm2-retail-detail-path-cdb.md) settled
/// which of retail's two detail paths real hardware runs. Retail's
/// RenderDevice::render_device.m_caps.bCanDoSinglePassDetailing reads
/// 1 and the file-static trysinglepass reads 1, so
/// D3DPolyRender::RenderMeshSubset (0x0059ca10) never falls back to
/// the two-pass framebuffer blend the earlier #226 port reproduced; it takes
/// the single-pass texture-stage combine set up in
/// D3DPolyRender::SetSurface (0x0059c4d0):
/// lerp(base * diffuse, detail.rgb, detail.a * diffuse.a) — a blend
/// TOWARD the detail colour by detail.a * diffuse.a, not the
/// fallback's dest * (detail.rgb + 1 - detail.a). Built meshes light
/// with tmpmaterial.Diffuse.a = 1 for opaque subsets
/// (RenderMeshSubset), so on the live Dereth category texture (mean
/// rgb 0.165, mean alpha 0.132) the combine is a mild darkening
/// (≈ 0.868 * base + 0.022), the opposite sign of the fallback's
/// brightening.
///
/// There is no distance fade on this path. Retail's
/// ACRender::get_alpha_for_z (0x006b6230) is only evaluated in
/// D3DPolyRender::DrawPolyInternal (0x0059d7c0, the immediate-polygon
/// path) and only when the static noFadeDetail (0x00820e38,
/// initialised to 1) is 0. Every loaded CGfxObj sets
/// use_built_mesh=1 (CGfxObj::InitLoad 0x005346b0), so buildings
/// and EnvCells never reach that function — their attenuation is the LINEAR
/// mip chain converging to the texture mean, not a scripted ramp.
///
internal static class RetailDetailTextureContract
{
internal static bool ShouldRender(
bool settingEnabled,
TerrainAtlas.RetailDetailTextureBinding binding) =>
settingEnabled && binding.IsAvailable;
///
/// Opaque detail must compare equal against the depth written by its exact
/// base geometry. On an MSAA target that inherits the base pass's per-sample
/// alpha-to-coverage mask without applying A2C to the detail alpha itself.
/// Transparent bases do not write depth, so their adjacent detail uses the
/// accepted less-or-equal comparison instead.
///
internal static GpuCompareOp DetailDepthCompare(bool transparent) =>
transparent ? GpuCompareOp.LessOrEqual : GpuCompareOp.Equal;
///
/// The exact pixel mesh_detail composites onto the existing
/// framebuffer colour: retail's single-pass stage-1
/// BLENDCURRENTALPHA(TEXTURE, CURRENT), a lerp from
/// toward 's RGB by
/// detail.a * opacity. is the base
/// subset's diffuse alpha — 1 for an opaque subset, the translucency-fade
/// multiplier for a fading one — mirrored from the shader's
/// instanceAlpha[instanceIndex] read.
///
internal static Vector3 Expected(Vector3 baseColour, Vector4 detail, float opacity) =>
Vector3.Lerp(
baseColour,
new Vector3(detail.X, detail.Y, detail.Z),
detail.W * opacity);
///
/// True when the combine above is an exact no-op — either the detail
/// texel is fully transparent or the base subset's own diffuse alpha (the
/// translucency fade) has reached zero. Neutral is detail.a * opacity
/// == 0, not any particular colour equality.
///
internal static bool IsNeutral(Vector4 detail, float opacity) =>
detail.W * opacity == 0f;
///
/// Review fix (post-05970306): retail's D3D fog stage runs AFTER the
/// texture-stage combine, applying to the FINAL pixel, not to
/// detail.rgb in isolation. acdream draws the combine as two
/// separate passes (mesh_modern's base draw, then mesh_detail's blended
/// replay), so each draw fogs its OWN colour before the fixed-function
/// blend recombines them — this is the CPU statement of that two-draw
/// path: lerp(mix(base,fog,f), mix(detail,fog,f), detail.a*opacity).
/// It is algebraically identical to retail's single-draw
/// fog-after-combine order, mix(Expected(base,detail,opacity), fog,
/// f) — see RetailDetailTextureContractTests for the identity
/// pinned numerically, and mesh_detail.frag's header comment for the
/// derivation.
///
internal static Vector3 ExpectedFogged(
Vector3 baseColour,
Vector4 detail,
float opacity,
Vector3 fog,
float fogFactor)
{
Vector3 foggedBase = Vector3.Lerp(baseColour, fog, fogFactor);
Vector3 foggedDetail = Vector3.Lerp(
new Vector3(detail.X, detail.Y, detail.Z),
fog,
fogFactor);
return Vector3.Lerp(foggedBase, foggedDetail, detail.W * opacity);
}
}