feat(D.5.1): IconComposer — CPU alpha-over icon composite + cache
Adds IconComposer (AcDream.App.UI) which mirrors retail IconData::RenderIcons (decomp 407524): decodes each RenderSurface layer directly via SurfaceDecoder, composites them bottom-to-top with Porter-Duff alpha-over, and uploads the result to a GL texture via TextureCache. Composited handles are keyed by the (iconId, underlayId, overlayId) tuple so each unique combo is uploaded once. Adds a public TextureCache.UploadRgba8(byte[], int, int, bool) wrapper — a thin shell around the existing private overload — so IconComposer can upload its CPU-side composite without duplicating any GL state logic. Pure Compose() path is covered by 2 unit tests (opaque top wins; transparent top preserves bottom). Dat-decode + GL-upload exercised by the visual gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6c485c2f06
commit
6e82807863
3 changed files with 129 additions and 0 deletions
|
|
@ -542,6 +542,11 @@ public sealed unsafe class TextureCache : Wb.ITextureCachePerInstance, IDisposab
|
|||
return composed;
|
||||
}
|
||||
|
||||
/// <summary>Uploads a raw RGBA8 byte array as a Texture2D. Used by
|
||||
/// <see cref="AcDream.App.UI.IconComposer"/> to upload CPU-composited icon layers.</summary>
|
||||
public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest = false)
|
||||
=> UploadRgba8(new DecodedTexture(rgba, width, height), nearest);
|
||||
|
||||
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
|
||||
{
|
||||
uint tex = _gl.GenTexture();
|
||||
|
|
|
|||
88
src/AcDream.App/UI/IconComposer.cs
Normal file
88
src/AcDream.App/UI/IconComposer.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Textures;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Builds an item icon by alpha-compositing its RenderSurface layers into one 32x32
|
||||
/// texture, mirroring retail IconData::RenderIcons (decomp 407524). Each layer is a
|
||||
/// 0x06 RenderSurface decoded DIRECTLY (the D.2b RenderSurface-vs-Surface rule).
|
||||
/// Phase 1 composites the layers ItemInstance exposes (custom underlay + base +
|
||||
/// custom overlay); the GetByEnum type-default underlay, the overlay ReplaceColor
|
||||
/// tint, and the effect overlay are deferred (see plan Task 12 / divergence rows).
|
||||
/// Composited textures are cached by their layer-id tuple.
|
||||
/// </summary>
|
||||
public sealed class IconComposer
|
||||
{
|
||||
private readonly DatCollection _dats;
|
||||
private readonly TextureCache _cache;
|
||||
private readonly Dictionary<(uint, uint, uint), uint> _byTuple = new();
|
||||
|
||||
public IconComposer(DatCollection dats, TextureCache cache)
|
||||
{
|
||||
_dats = dats;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
/// <summary>Pure alpha-over composite, bottom->top. Layers may differ in size;
|
||||
/// the result is sized to the FIRST (bottom) layer and upper layers are sampled
|
||||
/// top-left aligned (all icon layers are 32x32 in practice).</summary>
|
||||
public static (byte[] rgba, int w, int h) Compose(IReadOnlyList<(byte[] rgba, int w, int h)> layers)
|
||||
{
|
||||
if (layers.Count == 0) return (Array.Empty<byte>(), 0, 0);
|
||||
var (baseRgba, w, h) = layers[0];
|
||||
var outp = (byte[])baseRgba.Clone();
|
||||
for (int li = 1; li < layers.Count; li++)
|
||||
{
|
||||
var (src, sw, sh) = layers[li];
|
||||
int cw = Math.Min(w, sw), ch = Math.Min(h, sh);
|
||||
for (int y = 0; y < ch; y++)
|
||||
for (int x = 0; x < cw; x++)
|
||||
{
|
||||
int di = (y * w + x) * 4, si = (y * sw + x) * 4;
|
||||
float sa = src[si + 3] / 255f;
|
||||
if (sa <= 0f) continue;
|
||||
float da = 1f - sa;
|
||||
outp[di] = (byte)(src[si] * sa + outp[di] * da);
|
||||
outp[di + 1] = (byte)(src[si + 1] * sa + outp[di + 1] * da);
|
||||
outp[di + 2] = (byte)(src[si + 2] * sa + outp[di + 2] * da);
|
||||
outp[di + 3] = (byte)Math.Min(255f, src[si + 3] + outp[di + 3] * da);
|
||||
}
|
||||
}
|
||||
return (outp, w, h);
|
||||
}
|
||||
|
||||
/// <summary>Resolve (and cache) the composited GL texture for an item's icon
|
||||
/// layers. Returns 0 if no base icon is available.</summary>
|
||||
public uint GetIcon(uint iconId, uint underlayId, uint overlayId)
|
||||
{
|
||||
if (iconId == 0) return 0;
|
||||
var key = (iconId, underlayId, overlayId);
|
||||
if (_byTuple.TryGetValue(key, out var tex)) return tex;
|
||||
|
||||
var layers = new List<(byte[] rgba, int w, int h)>();
|
||||
AddLayer(layers, underlayId);
|
||||
AddLayer(layers, iconId);
|
||||
AddLayer(layers, overlayId);
|
||||
if (layers.Count == 0) return 0;
|
||||
|
||||
var (rgba, w, h) = Compose(layers);
|
||||
uint handle = _cache.UploadRgba8(rgba, w, h, nearest: true);
|
||||
_byTuple[key] = handle;
|
||||
return handle;
|
||||
}
|
||||
|
||||
private void AddLayer(List<(byte[], int, int)> layers, uint renderSurfaceId)
|
||||
{
|
||||
if (renderSurfaceId == 0) return;
|
||||
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var rs) &&
|
||||
!_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out rs))
|
||||
return;
|
||||
var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null);
|
||||
layers.Add((decoded.Rgba8, decoded.Width, decoded.Height));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue