From 180b4af2a96a839913b6cf292ff4e5cd79d576af Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 18 Jun 2026 17:19:27 +0200 Subject: [PATCH] =?UTF-8?q?refactor(lighting):=20extract=20GlobalLightPack?= =?UTF-8?q?er=20(shared=20binding=3D4=20layout)=20=E2=80=94=20A7=20Fix=20D?= =?UTF-8?q?=20prep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Rendering/Wb/WbDrawDispatcher.cs | 37 ++----------- .../Lighting/GlobalLightPacker.cs | 55 +++++++++++++++++++ .../Lighting/GlobalLightPackerTests.cs | 45 +++++++++++++++ 3 files changed, 105 insertions(+), 32 deletions(-) create mode 100644 src/AcDream.Core/Lighting/GlobalLightPacker.cs create mode 100644 tests/AcDream.Core.Tests/Lighting/GlobalLightPackerTests.cs diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 6fbc3cd6..fa686b3c 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -142,7 +142,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable private uint _globalLightsSsbo; private uint _instLightSetSsbo; private int[] _lightSetData = new int[256 * LightManager.MaxLightsPerObject]; - private float[] _globalLightData = new float[16 * 16]; // 16 floats (4 vec4) per GlobalLight + private float[] _globalLightData = new float[GlobalLightPacker.FloatsPerLight * 16]; // 16 floats (4 vec4) per GlobalLight // This frame's point-light snapshot, handed in by GameWindow before Draw via // SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1). private IReadOnlyList? _pointSnapshot; @@ -1812,39 +1812,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable /// private unsafe void UploadGlobalLights() { - var snap = _pointSnapshot; - int n = snap?.Count ?? 0; + int n = GlobalLightPacker.Pack(_pointSnapshot, ref _globalLightData); int count = n > 0 ? n : 1; // never zero-size - int floatsNeeded = count * 16; - if (_globalLightData.Length < floatsNeeded) - _globalLightData = new float[floatsNeeded + 16 * 16]; - Array.Clear(_globalLightData, 0, floatsNeeded); - - for (int i = 0; i < n; i++) - { - var L = snap![i]; - int o = i * 16; - // posAndKind (xyz world pos, w kind) - _globalLightData[o + 0] = L.WorldPosition.X; - _globalLightData[o + 1] = L.WorldPosition.Y; - _globalLightData[o + 2] = L.WorldPosition.Z; - _globalLightData[o + 3] = (int)L.Kind; - // dirAndRange (xyz forward, w range = Falloff×1.3) - _globalLightData[o + 4] = L.WorldForward.X; - _globalLightData[o + 5] = L.WorldForward.Y; - _globalLightData[o + 6] = L.WorldForward.Z; - _globalLightData[o + 7] = L.Range; - // colorAndIntensity (xyz linear colour, w intensity) - _globalLightData[o + 8] = L.ColorLinear.X; - _globalLightData[o + 9] = L.ColorLinear.Y; - _globalLightData[o + 10] = L.ColorLinear.Z; - _globalLightData[o + 11] = L.Intensity; - // coneAngleEtc (x cone radians; yzw reserved) - _globalLightData[o + 12] = L.ConeAngle; - } - + // Pack guarantees _globalLightData holds at least max(n,1) * FloatsPerLight floats. fixed (float* gp = _globalLightData) - UploadSsbo(_globalLightsSsbo, 4, gp, count * 16 * sizeof(float)); + UploadSsbo(_globalLightsSsbo, 4, gp, + count * GlobalLightPacker.FloatsPerLight * sizeof(float)); } /// diff --git a/src/AcDream.Core/Lighting/GlobalLightPacker.cs b/src/AcDream.Core/Lighting/GlobalLightPacker.cs new file mode 100644 index 00000000..9de709a5 --- /dev/null +++ b/src/AcDream.Core/Lighting/GlobalLightPacker.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace AcDream.Core.Lighting; + +/// +/// Packs a point-light snapshot into the flat float layout the bindless mesh +/// shader reads at SSBO binding=4 (mesh_modern.vert GlobalLight gLights[]): +/// 16 floats (4 vec4) per light — posAndKind, dirAndRange, colorAndIntensity, +/// coneAngleEtc. Pure (no GL), so both WbDrawDispatcher and +/// EnvCellRenderer share ONE layout and cannot drift. +/// +public static class GlobalLightPacker +{ + public const int FloatsPerLight = 16; + + /// + /// Fill (grown + zero-cleared as needed) with the + /// packed snapshot; returns the light count n. The buffer always has at + /// least floats (so a zero-light frame still + /// uploads a non-empty SSBO). Callers upload max(n,1) * FloatsPerLight floats. + /// + public static int Pack(IReadOnlyList? snapshot, ref float[] buffer) + { + int n = snapshot?.Count ?? 0; + int floatsNeeded = Math.Max(n, 1) * FloatsPerLight; + if (buffer.Length < floatsNeeded) + buffer = new float[floatsNeeded + FloatsPerLight * 16]; + Array.Clear(buffer, 0, floatsNeeded); + + for (int i = 0; i < n; i++) + { + var L = snapshot![i]; + int o = i * FloatsPerLight; + // posAndKind (xyz world pos, w kind) + buffer[o + 0] = L.WorldPosition.X; + buffer[o + 1] = L.WorldPosition.Y; + buffer[o + 2] = L.WorldPosition.Z; + buffer[o + 3] = (int)L.Kind; + // dirAndRange (xyz forward, w range) + buffer[o + 4] = L.WorldForward.X; + buffer[o + 5] = L.WorldForward.Y; + buffer[o + 6] = L.WorldForward.Z; + buffer[o + 7] = L.Range; // w = Range = Falloff × static_light_factor (1.3), pre-multiplied by LightInfoLoader — NOT the raw dat Falloff + // colorAndIntensity (xyz linear colour, w intensity) + buffer[o + 8] = L.ColorLinear.X; + buffer[o + 9] = L.ColorLinear.Y; + buffer[o + 10] = L.ColorLinear.Z; + buffer[o + 11] = L.Intensity; + // coneAngleEtc (x cone radians; yzw reserved) + buffer[o + 12] = L.ConeAngle; + } + return n; + } +} diff --git a/tests/AcDream.Core.Tests/Lighting/GlobalLightPackerTests.cs b/tests/AcDream.Core.Tests/Lighting/GlobalLightPackerTests.cs new file mode 100644 index 00000000..174c4c41 --- /dev/null +++ b/tests/AcDream.Core.Tests/Lighting/GlobalLightPackerTests.cs @@ -0,0 +1,45 @@ +using System.Numerics; +using AcDream.Core.Lighting; +using Xunit; + +namespace AcDream.Core.Tests.Lighting; + +public class GlobalLightPackerTests +{ + [Fact] + public void Pack_WritesSixteenFloatsPerLight_InTheExpectedLayout() + { + var light = new LightSource + { + Kind = LightKind.Point, + WorldPosition = new Vector3(10f, 20f, 30f), + WorldForward = new Vector3(0f, 0f, 1f), + ColorLinear = new Vector3(1.0f, 0.588f, 0.314f), + Intensity = 100f, + Range = 5.2f, + ConeAngle = 0f, + }; + float[] buffer = System.Array.Empty(); + + int count = GlobalLightPacker.Pack(new[] { light }, ref buffer); + + Assert.Equal(1, count); + Assert.True(buffer.Length >= 16); + Assert.Equal(10f, buffer[0]); Assert.Equal(20f, buffer[1]); Assert.Equal(30f, buffer[2]); + Assert.Equal((float)(int)LightKind.Point, buffer[3]); + Assert.Equal(0f, buffer[4]); Assert.Equal(0f, buffer[5]); Assert.Equal(1f, buffer[6]); + Assert.Equal(5.2f, buffer[7]); + Assert.Equal(1.0f, buffer[8]); Assert.Equal(0.588f, buffer[9]); Assert.Equal(0.314f, buffer[10]); + Assert.Equal(100f, buffer[11]); + Assert.Equal(0f, buffer[12]); + } + + [Fact] + public void Pack_NullOrEmpty_ReturnsZero_AndBufferHasAtLeastOneSlot() + { + float[] buffer = System.Array.Empty(); + int count = GlobalLightPacker.Pack(null, ref buffer); + Assert.Equal(0, count); + Assert.True(buffer.Length >= GlobalLightPacker.FloatsPerLight); + } +}