namespace AcDream.App.Rendering.Gpu.Vk; /// /// Campaign V slice V6b, plan §4.3: build a full mip chain on the CPU. /// /// DAT surfaces arrive with level 0 only. Uncompressed formats can have /// their chain generated on the GPU with vkCmdBlitImage; block-compressed /// ones cannot, because a compressed image is not a legal blit destination. So /// BC chains are built here, and the result is uploaded level by level exactly /// as level 0 was. /// /// The filter is a plain 2x2 box average, computed in 8-bit space with /// round-half-up. It is not a Gaussian or a Kaiser, and the reason is the same /// reason the encoder does not fit a principal axis: this has to produce the /// same bytes every run, on every machine, so the pixel gate compares renders /// rather than the output of a floating-point reduction that reassociated /// differently. Integer arithmetic throughout means it does. /// /// Alpha is averaged, not premultiplied. Retail's cut-out foliage /// uses BC1's one-bit alpha, and the encoder's three-colour mode preserves the /// cut at every level; premultiplying would darken the fringe of every leaf as /// the chain descended. /// internal static class BlockCompressionMipChain { /// One level of a generated chain: its extent and its encoded bytes. internal readonly record struct Level(int MipLevel, int Width, int Height, byte[] Data); /// /// Builds levels 1..-1 from a compressed /// level 0. Level 0 itself is not returned — it was uploaded verbatim and /// re-encoding it would be a lossy round trip of data that is already right. /// internal static IReadOnlyList BuildCompressed( GpuTextureFormat format, ReadOnlySpan level0, int width, int height, int mipLevelCount) { if (!BlockCompressionCodec.IsBlockCompressed(format)) throw new ArgumentOutOfRangeException(nameof(format), format, "This chain builder is for BC formats."); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(mipLevelCount); if (mipLevelCount == 1) return []; byte[] rgba = BlockCompressionCodec.DecodeLevel(format, level0, width, height); return BuildFromRgba(format, rgba, width, height, mipLevelCount); } /// /// Builds levels 1..N-1 from a tightly packed RGBA8 level 0, encoding each /// into . Exposed separately because a caller that /// already has uncompressed source (a composited UI surface, a test) should /// not pay a decode to get here. /// internal static IReadOnlyList BuildFromRgba( GpuTextureFormat format, ReadOnlySpan rgba, int width, int height, int mipLevelCount) { var levels = new List(Math.Max(0, mipLevelCount - 1)); byte[] current = rgba.ToArray(); int currentWidth = width; int currentHeight = height; for (int level = 1; level < mipLevelCount; level++) { (byte[] next, int nextWidth, int nextHeight) = Downsample(current, currentWidth, currentHeight); byte[] encoded = BlockCompressionCodec.IsBlockCompressed(format) ? BlockCompressionCodec.EncodeLevel(format, next, nextWidth, nextHeight) : next; levels.Add(new Level(level, nextWidth, nextHeight, encoded)); current = next; currentWidth = nextWidth; currentHeight = nextHeight; } return levels; } /// /// Halves an RGBA8 image with a 2x2 box average. Odd extents clamp the /// second sample to the last row/column instead of wrapping or reading out /// of bounds — a non-power-of-two DAT surface is unusual but not impossible, /// and the alternative is a buffer overrun rather than a slightly soft mip. /// internal static (byte[] Rgba, int Width, int Height) Downsample( ReadOnlySpan rgba, int width, int height) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(width); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(height); int nextWidth = Math.Max(1, width / 2); int nextHeight = Math.Max(1, height / 2); var output = new byte[nextWidth * nextHeight * 4]; for (int y = 0; y < nextHeight; y++) { int y0 = Math.Min((y * 2) + 0, height - 1); int y1 = Math.Min((y * 2) + 1, height - 1); for (int x = 0; x < nextWidth; x++) { int x0 = Math.Min((x * 2) + 0, width - 1); int x1 = Math.Min((x * 2) + 1, width - 1); int a = ((y0 * width) + x0) * 4; int b = ((y0 * width) + x1) * 4; int c = ((y1 * width) + x0) * 4; int d = ((y1 * width) + x1) * 4; int destination = ((y * nextWidth) + x) * 4; for (int channel = 0; channel < 4; channel++) { int sum = rgba[a + channel] + rgba[b + channel] + rgba[c + channel] + rgba[d + channel]; // Round half up in integers so the result never depends on // a floating-point rounding mode. output[destination + channel] = (byte)((sum + 2) / 4); } } } return (output, nextWidth, nextHeight); } }