Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the interior seals (live-verified by the user). Commits the session render-rewrite foundation together with the fixes that made it functional. - HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept (load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit); only its count is capped. CellViewDedupTests added. - Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey). - Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81 loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled). Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway, confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight): docs/research/2026-06-07-indoor-render-session-handoff.md. Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
254 lines
8.9 KiB
C#
254 lines
8.9 KiB
C#
// TextureDump — probe rain-streak textures (0x050016A4..0x050016A8) for
|
|
// the volumetric rain density-trick decision.
|
|
using System;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using AcDream.Core.Textures;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Options;
|
|
using SysEnv = System.Environment;
|
|
|
|
string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
|
?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile),
|
|
"Documents", "Asheron's Call");
|
|
|
|
Console.WriteLine($"datDir = {datDir}");
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
|
|
string outDir = Path.Combine(AppContext.BaseDirectory, "out");
|
|
Directory.CreateDirectory(outDir);
|
|
Console.WriteLine($"outDir = {outDir}");
|
|
|
|
uint[] ids;
|
|
if (args.Length > 0)
|
|
{
|
|
ids = args.Select(ParseId).ToArray();
|
|
}
|
|
else
|
|
{
|
|
// Original ask: 0x050016A4..0x050016A8. A4/A5/A7/A8 don't exist as files; widen
|
|
// to 0x050016A0..0x050016AF to catch any related precip textures.
|
|
var idList = new System.Collections.Generic.List<uint>();
|
|
for (uint i = 0x050016A0; i <= 0x050016AF; i++) idList.Add(i);
|
|
ids = idList.ToArray();
|
|
}
|
|
|
|
(uint id, double densityFraction)? best = null;
|
|
|
|
foreach (var id in ids)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine($"=== 0x{id:X8} ===");
|
|
|
|
RenderSurface? rs = null;
|
|
uint lookupId = id;
|
|
if (dats.TryGet<Surface>(id, out var surface) && surface is not null)
|
|
{
|
|
lookupId = (uint)surface.OrigTextureId;
|
|
var color = surface.ColorValue is null
|
|
? "null"
|
|
: $"0x{surface.ColorValue.Alpha:X2}{surface.ColorValue.Red:X2}{surface.ColorValue.Green:X2}{surface.ColorValue.Blue:X2}";
|
|
Console.WriteLine($" Surface descriptor, type={surface.Type}, color={color}, origTexture=0x{lookupId:X8}");
|
|
}
|
|
|
|
// SurfaceTexture wrapper (0x05xxxxxx) → first inner RenderSurface (0x06xxxxxx).
|
|
if (dats.TryGet<SurfaceTexture>(lookupId, out var st) && st is not null && st.Textures.Count > 0)
|
|
{
|
|
uint rsid = (uint)st.Textures[0];
|
|
Console.WriteLine($" SurfaceTexture wrapper, {st.Textures.Count} mip(s), first = 0x{rsid:X8}");
|
|
if (dats.TryGet<RenderSurface>(rsid, out var inner) && inner is not null)
|
|
rs = inner;
|
|
}
|
|
else if (dats.TryGet<RenderSurface>(lookupId, out var direct) && direct is not null)
|
|
{
|
|
rs = direct;
|
|
}
|
|
if (rs is null)
|
|
{
|
|
Console.WriteLine(" (not a SurfaceTexture or RenderSurface, or not found)");
|
|
continue;
|
|
}
|
|
|
|
Console.WriteLine($" Dimensions: {rs.Width} x {rs.Height}");
|
|
Console.WriteLine($" PixelFormat: {rs.Format} (0x{(uint)rs.Format:X8})");
|
|
Console.WriteLine($" SourceData bytes: {rs.SourceData?.Length ?? 0}");
|
|
Console.WriteLine($" DefaultPaletteId: 0x{rs.DefaultPaletteId:X8}");
|
|
|
|
Palette? palette = null;
|
|
if (rs.DefaultPaletteId != 0)
|
|
{
|
|
if (dats.TryGet<Palette>(rs.DefaultPaletteId, out var pal) && pal is not null)
|
|
{
|
|
palette = pal;
|
|
Console.WriteLine($" Palette colors: {pal.Colors.Count}");
|
|
}
|
|
}
|
|
|
|
var dec = SurfaceDecoder.DecodeRenderSurface(rs, palette);
|
|
if (dec.Rgba8 is null || dec.Rgba8.Length == 0)
|
|
{
|
|
Console.WriteLine(" DECODE FAILED.");
|
|
continue;
|
|
}
|
|
|
|
// Stats: alpha histogram (8 buckets), brightness mean/stddev, bright count.
|
|
int n = dec.Width * dec.Height;
|
|
int[] alphaBuckets = new int[8]; // 0..31, 32..63, ..., 224..255
|
|
long sumB = 0;
|
|
long sumBSq = 0;
|
|
int brightAlpha = 0; // alpha > 128
|
|
int brightLum = 0; // brightness > 200
|
|
int litLum = 0; // brightness > 16 (any visible pixel)
|
|
int midLum = 0; // brightness > 64
|
|
int nonZeroAlpha = 0;
|
|
int maxLum = 0;
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
byte r = dec.Rgba8[i * 4 + 0];
|
|
byte g = dec.Rgba8[i * 4 + 1];
|
|
byte b = dec.Rgba8[i * 4 + 2];
|
|
byte a = dec.Rgba8[i * 4 + 3];
|
|
int lum = (r + g + b) / 3;
|
|
sumB += lum;
|
|
sumBSq += (long)lum * lum;
|
|
alphaBuckets[Math.Min(7, a / 32)]++;
|
|
if (a > 128) brightAlpha++;
|
|
if (lum > 200) brightLum++;
|
|
if (lum > 64) midLum++;
|
|
if (lum > 16) litLum++;
|
|
if (a > 0) nonZeroAlpha++;
|
|
if (lum > maxLum) maxLum = lum;
|
|
}
|
|
double mean = (double)sumB / n;
|
|
double variance = ((double)sumBSq / n) - mean * mean;
|
|
double stddev = Math.Sqrt(Math.Max(0, variance));
|
|
|
|
Console.WriteLine($" Pixels: {n}");
|
|
Console.Write(" Alpha histogram (8 buckets, 0->255):");
|
|
for (int b = 0; b < 8; b++) Console.Write($" {alphaBuckets[b]}");
|
|
Console.WriteLine();
|
|
Console.WriteLine($" Brightness mean = {mean:F2}, stddev = {stddev:F2}, max = {maxLum}");
|
|
Console.WriteLine($" Lit pixels: lum>16 = {litLum} ({100.0 * litLum / n:F3}%) lum>64 = {midLum} ({100.0 * midLum / n:F3}%) lum>200 = {brightLum} ({100.0 * brightLum / n:F3}%)");
|
|
Console.WriteLine($" alpha>128 = {brightAlpha} ({100.0 * brightAlpha / n:F2}%) nonZeroAlpha = {nonZeroAlpha} ({100.0 * nonZeroAlpha / n:F2}%)");
|
|
|
|
// "Density" for fake-volumetric look: % of pixels that contribute a visible
|
|
// streak. Use max(alpha>128, lum>200, nonZeroAlpha) as the proxy — different
|
|
// textures encode the streak via either alpha-clip or pure luminance.
|
|
double densityFrac = Math.Max(brightAlpha, Math.Max(brightLum, nonZeroAlpha)) / (double)n;
|
|
if (best is null || densityFrac > best.Value.densityFraction)
|
|
best = (id, densityFrac);
|
|
|
|
string pngPath = Path.Combine(outDir, $"tex_{id:X8}.png");
|
|
WritePng(pngPath, dec.Rgba8, dec.Width, dec.Height);
|
|
Console.WriteLine($" PNG: {pngPath}");
|
|
}
|
|
|
|
Console.WriteLine();
|
|
if (best is not null)
|
|
Console.WriteLine($"DENSEST: 0x{best.Value.id:X8} (density frac {best.Value.densityFraction:F4})");
|
|
return 0;
|
|
|
|
// -------- Minimal PNG encoder (RGBA8, no filter, single IDAT, zlib via DeflateStream) --------
|
|
static void WritePng(string path, byte[] rgba, int width, int height)
|
|
{
|
|
using var fs = File.Create(path);
|
|
// Signature
|
|
fs.Write(new byte[] { 0x89, (byte)'P', (byte)'N', (byte)'G', 0x0D, 0x0A, 0x1A, 0x0A });
|
|
|
|
// IHDR
|
|
var ihdr = new byte[13];
|
|
WriteBE(ihdr, 0, (uint)width);
|
|
WriteBE(ihdr, 4, (uint)height);
|
|
ihdr[8] = 8; // bit depth
|
|
ihdr[9] = 6; // color type RGBA
|
|
ihdr[10] = 0; // compression
|
|
ihdr[11] = 0; // filter
|
|
ihdr[12] = 0; // interlace
|
|
WriteChunk(fs, "IHDR", ihdr);
|
|
|
|
// IDAT: rows prefixed with filter byte 0, zlib-wrapped deflate.
|
|
using var raw = new MemoryStream();
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
raw.WriteByte(0);
|
|
raw.Write(rgba, y * width * 4, width * 4);
|
|
}
|
|
byte[] uncompressed = raw.ToArray();
|
|
|
|
using var compressed = new MemoryStream();
|
|
// zlib header: 0x78 0x9C (deflate, default compression)
|
|
compressed.WriteByte(0x78);
|
|
compressed.WriteByte(0x9C);
|
|
using (var deflate = new DeflateStream(compressed, CompressionLevel.Fastest, leaveOpen: true))
|
|
deflate.Write(uncompressed, 0, uncompressed.Length);
|
|
// Adler-32 of uncompressed data, big-endian.
|
|
uint adler = Adler32(uncompressed);
|
|
compressed.WriteByte((byte)(adler >> 24));
|
|
compressed.WriteByte((byte)(adler >> 16));
|
|
compressed.WriteByte((byte)(adler >> 8));
|
|
compressed.WriteByte((byte)adler);
|
|
|
|
WriteChunk(fs, "IDAT", compressed.ToArray());
|
|
WriteChunk(fs, "IEND", Array.Empty<byte>());
|
|
}
|
|
|
|
static void WriteBE(byte[] buf, int offset, uint v)
|
|
{
|
|
buf[offset + 0] = (byte)(v >> 24);
|
|
buf[offset + 1] = (byte)(v >> 16);
|
|
buf[offset + 2] = (byte)(v >> 8);
|
|
buf[offset + 3] = (byte)v;
|
|
}
|
|
|
|
static void WriteChunk(Stream s, string type, byte[] data)
|
|
{
|
|
var len = new byte[4];
|
|
WriteBE(len, 0, (uint)data.Length);
|
|
s.Write(len);
|
|
var typeBytes = System.Text.Encoding.ASCII.GetBytes(type);
|
|
s.Write(typeBytes);
|
|
s.Write(data);
|
|
var crcInput = new byte[typeBytes.Length + data.Length];
|
|
Buffer.BlockCopy(typeBytes, 0, crcInput, 0, typeBytes.Length);
|
|
Buffer.BlockCopy(data, 0, crcInput, typeBytes.Length, data.Length);
|
|
uint crc = Crc32(crcInput);
|
|
s.WriteByte((byte)(crc >> 24));
|
|
s.WriteByte((byte)(crc >> 16));
|
|
s.WriteByte((byte)(crc >> 8));
|
|
s.WriteByte((byte)crc);
|
|
}
|
|
|
|
static uint Crc32(byte[] data)
|
|
{
|
|
uint c = 0xFFFFFFFFu;
|
|
foreach (var b in data)
|
|
{
|
|
uint v = (c ^ b) & 0xFF;
|
|
for (int k = 0; k < 8; k++)
|
|
v = ((v & 1) != 0) ? (0xEDB88320u ^ (v >> 1)) : (v >> 1);
|
|
c = v ^ (c >> 8);
|
|
}
|
|
return c ^ 0xFFFFFFFFu;
|
|
}
|
|
|
|
static uint Adler32(byte[] data)
|
|
{
|
|
const uint MOD = 65521;
|
|
uint a = 1, b = 0;
|
|
foreach (var x in data)
|
|
{
|
|
a = (a + x) % MOD;
|
|
b = (b + a) % MOD;
|
|
}
|
|
return (b << 16) | a;
|
|
}
|
|
|
|
static uint ParseId(string text)
|
|
{
|
|
text = text.Trim();
|
|
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
return Convert.ToUInt32(text[2..], 16);
|
|
return Convert.ToUInt32(text, 16);
|
|
}
|