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);
}
}