From 45335923c000c756167f71765b16d3a08f82b988 Mon Sep 17 00:00:00 2001 From: Zemogiter Date: Sun, 26 Jul 2026 02:39:35 +0200 Subject: [PATCH 1/8] Blueprint Generation Overhaul Implemented GPU-accelerated mesh voxelization using ILGPU. This means faster blueprint generation. For instance this huge model https://sketchfab.com/3d-models/kyogre-265b2838b1824ec599274cef63a5b906 is converted to a blueprint in just under 15 seconds (RX 9070XT, all slope settings, 2.5m blocks. model size 638 - not changed by me) vs over 20 via the old method. Other features including: - Progress reporting with cancellable operations (Esc key) - CPU fallback mode available via Shift+click, the text under the model size informs the user what mode is used. - Enhanced UI with progress bar and generation status. Jumps in place of the generate button when a blueprint generation is in place, colapses and restores the button once the blueprint is ready. - Renamed original Generate() to GenerateCpu() for clarity. - Added thread-safe generation state tracking to prevent concurrent operations - Minor typo fixes in model size warning messages --- Algorithms/GridShaper.cs | 838 +++++++++++++++++++--------- Controls/BlueprintGenerator.xaml | 41 +- Controls/BlueprintGenerator.xaml.cs | 106 +++- SpaceEditor.csproj | 25 +- 4 files changed, 720 insertions(+), 290 deletions(-) diff --git a/Algorithms/GridShaper.cs b/Algorithms/GridShaper.cs index 1757988..ed811dc 100644 --- a/Algorithms/GridShaper.cs +++ b/Algorithms/GridShaper.cs @@ -1,15 +1,37 @@ using Assimp; using g4; +using ILGPU; +using ILGPU.Runtime; +using PropertyTools.DataAnnotations; +using SpaceEditor.Algorithms; using SpaceEditor.Rocks; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Numerics; using System.Text; -using PropertyTools.DataAnnotations; namespace SpaceEditor.Algorithms; +// Unmanaged, SIMD-free representation of a 3D vector for ILGPU +public struct Float3 +{ + public float X, Y, Z; + public Float3(float x, float y, float z) { X = x; Y = y; Z = z; } +} + +// Unmanaged representation of a triangle for the GPU +public struct GpuTriangle +{ + public Float3 V0; + public Float3 V1; + public Float3 V2; + public Float3 MinBounds; + public Float3 MaxBounds; +} + public class GridShaper { public DMesh3 Mesh { get; } @@ -32,7 +54,7 @@ public class GeneratorSettings { public bool SlopesUpper { get; set; } = true; public bool SlopesLower { get; set; } = true; - public bool SlopesSides { get; set; } = true; + public bool SlopesSides { get; set; } = true; public bool SlopesMustBeSupported { get; set; } = false; [ItemsSourceProperty(nameof(BlockSizeValues))] @@ -47,7 +69,192 @@ public class GeneratorSettings ]; } - public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct) + public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, IProgress<(double, string)> progress = null) + { + var blockSize = settings.BlockSize switch + { + BlockSizes.TwoPointFive => ShapeDB.LargeBlockSize, + BlockSizes.HalfMeter => ShapeDB.MidBlockSize + }; + + var minimalBounds = this.Tree.Bounds; + minimalBounds.Min -= blockSize; + minimalBounds.Max += blockSize; + + var boundingBox = new g4.AxisAlignedBox3d(new g4.Vector3d(0), blockSize / 2); + while (boundingBox.Contains(minimalBounds) == false) + { + boundingBox.Scale(2, 2, 2); + } + + var cellCount = (int)Math.Ceiling(boundingBox.MaxDim / blockSize); + var indexer = new ShiftGridIndexer3(boundingBox.Min, blockSize); + + // Initialize ILGPU Context + using var context = Context.CreateDefault(); + using var accelerator = context.GetPreferredDevice(preferCPU: false).CreateAccelerator(context); + + // Prepare Triangle Data + int triangleCount = this.Mesh.TriangleCount; + var flatTriangles = new GpuTriangle[triangleCount]; + int tIndex = 0; + + // PHASE 1: Triangles (0% - 10%) + foreach (var triangle in this.Mesh.EnumerateTriangles()) + { + ct.ThrowIfCancellationRequested(); + // Report progress periodically to avoid UI thread spam + if (tIndex % 5000 == 0) progress?.Report((0.1 * ((double)tIndex / triangleCount), "Mesh Flattening...")); + + var box = triangle.ToBox(); + flatTriangles[tIndex++] = new GpuTriangle + { + V0 = new Float3((float)triangle.V0.x, (float)triangle.V0.y, (float)triangle.V0.z), + V1 = new Float3((float)triangle.V1.x, (float)triangle.V1.y, (float)triangle.V1.z), + V2 = new Float3((float)triangle.V2.x, (float)triangle.V2.y, (float)triangle.V2.z), + MinBounds = new Float3((float)box.Min.x, (float)box.Min.y, (float)box.Min.z), + MaxBounds = new Float3((float)box.Max.x, (float)box.Max.y, (float)box.Max.z) + }; + } + + // Allocate GPU Memory + using var deviceTriangles = accelerator.Allocate1D(flatTriangles); + + // Flat 1D representation of the 3D voxel grid + int totalCells = cellCount * cellCount * cellCount; + int[] initialGrid = new int[totalCells]; + Array.Fill(initialGrid, BlueprintMesh.NoContent); + using var deviceGrid = accelerator.Allocate1D(initialGrid); + + // Load and compile kernel + var voxelizeKernel = accelerator.LoadAutoGroupedStreamKernel< + Index1D, ArrayView, ArrayView, Float3, float, int, int, int>( + VoxelizationKernel.Voxelize); + + Float3 origin = new Float3((float)boundingBox.Min.x, (float)boundingBox.Min.y, (float)boundingBox.Min.z); + + // Dispatch execution to the GPU + voxelizeKernel( + deviceTriangles.IntExtent, + deviceTriangles.View, + deviceGrid.View, + origin, + blockSize, + cellCount, + cellCount, + cellCount + ); + + // PHASE 2: Voxelization (Jump to 40% after sync) + accelerator.Synchronize(); + progress?.Report((0.40, "Voxelization...")); + var flatResults = deviceGrid.GetAsArray1D(); + + // Reconstruct the internal BlueprintMesh data structure + var bmp = new DenseGrid3i(cellCount, cellCount, cellCount, BlueprintMesh.NoContent); + + // PHASE 3: Grid Reconstruction (40% - 70%) + for (int z = 0; z < cellCount; z++) + { + ct.ThrowIfCancellationRequested(); + if (z % 10 == 0) progress?.Report((0.40 + (0.30 * ((double)z / cellCount)), "Grid Reconstruction...")); + + for (int y = 0; y < cellCount; y++) + { + for (int x = 0; x < cellCount; x++) + { + int flatIdx = x + (y * cellCount) + (z * cellCount * cellCount); + if (flatResults[flatIdx] == 0) + { + bmp[new g4.Vector3i(x, y, z)] = 0; + } + } + } + } + + var blueprint = new BlueprintMesh(); + blueprint.Blocks = bmp; + blueprint.Coords = indexer; + blueprint.Shapes = settings.BlockSize switch + { + BlockSizes.TwoPointFive => ShapeDB.LargeShapes, + BlockSizes.HalfMeter => ShapeDB.MidShapes + }; + + // PHASE 4: Slope Generation (70% - 100%) + progress?.Report((0.70, "Slope Evaluation...")); + int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); + int executedPasses = 0; + + if (settings.SlopesUpper) + { + ExecSlopes(1); + ExecSlopes(2); + ExecSlopes(3); + ExecSlopes(4); + } + + if (settings.SlopesLower) + { + ExecSlopes(5); + ExecSlopes(6); + ExecSlopes(7); + ExecSlopes(8); + } + + if (settings.SlopesSides) + { + ExecSlopes(9); + ExecSlopes(10); + ExecSlopes(11); + ExecSlopes(12); + } + + void ExecSlopes(int content) + { + var shapeInfo = blueprint.Shapes[content]; + var probeDirectionA = -Base6Directions.Vectors[shapeInfo.Forward]; + var probeDirectionB = Base6Directions.Vectors[shapeInfo.Up]; + var supportDirectionA = -probeDirectionA; + var supportDirectionB = -probeDirectionB; + + foreach (var g in bmp.Indices()) + { + ct.ThrowIfCancellationRequested(); + + if (blueprint[g] != 0) continue; + + if(blueprint[g + probeDirectionA] != BlueprintMesh.NoContent ||blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) + { + continue; + } + + if (settings.SlopesMustBeSupported) + { + ct.ThrowIfCancellationRequested(); + if(blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) + { + continue; + } + } + + bmp[g] = content; + } + } + + // Report progress after each directional pass completes + if (totalSlopePasses > 0) + { + executedPasses++; + progress?.Report((0.70 + (0.30 * ((double)executedPasses / totalSlopePasses)), "Slope Evaluation...")); + } + + progress?.Report((1.0, "Complete!")); + return blueprint; + } + + //Old, CPU based rendering. Kept here as a legacy option, in case using the GPU is not feasible for some reason. It is significantly slower than the GPU version, especially for large meshes. + public BlueprintMesh GenerateCpu(GeneratorSettings settings, CancellationToken ct, IProgress<(double, string)> progress = null) { var blockSize = settings.BlockSize switch { @@ -62,7 +269,6 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct) var boundingBox = new AxisAlignedBox3d(new Vector3d(0), blockSize / 2); while (boundingBox.Contains(minimalBounds) == false) { - //TODO: Fix block offset boundingBox.Scale(2, 2, 2); } @@ -79,9 +285,16 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct) BlockSizes.TwoPointFive => ShapeDB.LargeShapes, BlockSizes.HalfMeter => ShapeDB.MidShapes }; - + + int triangleCount = this.Mesh.TriangleCount; + int tIndex = 0; + foreach (var triangle in this.Mesh.EnumerateTriangles()) { + ct.ThrowIfCancellationRequested(); + if (tIndex % 1000 == 0) progress?.Report((0.7 * ((double)tIndex / triangleCount), "Triangle Evaluation...")); + tIndex++; + var triBox = triangle.ToBox(); foreach (var cell in Enumerators.BoxRange(triBox, indexer)) { @@ -93,6 +306,10 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct) } } + progress?.Report((0.70, "Dispatch & Execution...")); + int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); + int executedPasses = 0; + if (settings.SlopesUpper) { ExecSlopes(1); @@ -127,26 +344,18 @@ void ExecSlopes(int content) foreach (var g in bmp.Indices()) { + ct.ThrowIfCancellationRequested(); if (blueprint[g] != 0) continue; - if - ( - blueprint[g + probeDirectionA] != BlueprintMesh.NoContent || - blueprint[g + probeDirectionB] != BlueprintMesh.NoContent - ) + if(blueprint[g + probeDirectionA] != BlueprintMesh.NoContent || blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) { continue; } if (settings.SlopesMustBeSupported) { - if - ( - //TODO: Should use face check, something symmetric - blueprint[g + supportDirectionA] != 0 || - blueprint[g + supportDirectionB] != 0 - ) + if(blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) { continue; } @@ -154,305 +363,428 @@ void ExecSlopes(int content) bmp[g] = content; } + + if (totalSlopePasses > 0) + { + executedPasses++; + progress?.Report((0.70 + (0.30 * ((double)executedPasses / totalSlopePasses)), "Slope Evaluation...")); + } } + progress?.Report((1.0, "Complete!")); return blueprint; } } -public class GridMesher -{ - public static DMesh3 Mesh(BlueprintMesh blueprint) + public class GridMesher { - var grid = blueprint.Blocks; - - var cubes = new Bitmap3(new(grid.ni, grid.nj, grid.nk)); - foreach(var g in grid.Indices()) + public static DMesh3 Mesh(BlueprintMesh blueprint) { - cubes[g] = grid[g] == 0; - } + var grid = blueprint.Blocks; - var slopeMesh = new DMesh3(); - foreach (var g in grid.Indices()) - { - if (cubes[g]) - continue; + var cubes = new Bitmap3(new(grid.ni, grid.nj, grid.nk)); + foreach (var g in grid.Indices()) + { + cubes[g] = grid[g] == 0; + } - var shapeId = grid[g]; - if (shapeId == BlueprintMesh.NoContent) - continue; + var slopeMesh = new DMesh3(); + foreach (var g in grid.Indices()) + { + if (cubes[g]) + continue; - var shapeInfo = blueprint.Shapes[shapeId]; + var shapeId = grid[g]; + if (shapeId == BlueprintMesh.NoContent) + continue; - slopeMesh.AppendMesh - ( - shapeInfo.Shape, - MathRocks.ForwardUpTranslate + var shapeInfo = blueprint.Shapes[shapeId]; + + slopeMesh.AppendMesh ( - shapeInfo.Forward, - shapeInfo.Up, - (Vector3f) blueprint.Coords.ToBox(g).Center - ) - ); - } - - var cubesSurfaceGenerator = new VoxelSurfaceGenerator(); - cubesSurfaceGenerator.Voxels = cubes; - cubesSurfaceGenerator.Generate(); - - var cubesMesh = cubesSurfaceGenerator.Meshes[0]; - MeshTransforms.Scale(cubesMesh, blueprint.Coords.CellSize); + shapeInfo.Shape, + MathRocks.ForwardUpTranslate + ( + shapeInfo.Forward, + shapeInfo.Up, + (Vector3f)blueprint.Coords.ToBox(g).Center + ) + ); + } - // Voxel generator generates around UnitZeroCentered, while indexer rounds down to corner - var correctionOffset = blueprint.Coords.CellSize / 2; - MeshTransforms.Translate(cubesMesh, blueprint.Coords.Origin + correctionOffset); + var cubesSurfaceGenerator = new VoxelSurfaceGenerator(); + cubesSurfaceGenerator.Voxels = cubes; + cubesSurfaceGenerator.Generate(); + var cubesMesh = cubesSurfaceGenerator.Meshes[0]; + MeshTransforms.Scale(cubesMesh, blueprint.Coords.CellSize); - var finalMesh = cubesMesh; - finalMesh.AppendMesh(slopeMesh); + // Voxel generator generates around UnitZeroCentered, while indexer rounds down to corner + var correctionOffset = blueprint.Coords.CellSize / 2; + MeshTransforms.Translate(cubesMesh, blueprint.Coords.Origin + correctionOffset); - return finalMesh; - } -} -public class ShapeDB -{ - public const float LargeBlockSize = 2.5f; - public const float MidBlockSize = 0.5f; - public const float SmallBlockSize = 0.25f; + var finalMesh = cubesMesh; + finalMesh.AppendMesh(slopeMesh); - public record ShapeInfo + return finalMesh; + } + } + + public class ShapeDB { - public DMesh3 Shape; - public string Prefab; + public const float LargeBlockSize = 2.5f; + public const float MidBlockSize = 0.5f; + public const float SmallBlockSize = 0.25f; - public int Forward = Base6Directions.Forward; - public int Up = Base6Directions.Up; - } + public record ShapeInfo + { + public DMesh3 Shape; + public string Prefab; - public ShapeInfo[] Shapes { get; } - public ShapeInfo this[int index] => this.Shapes[index]; + public int Forward = Base6Directions.Forward; + public int Up = Base6Directions.Up; + } - public ShapeDB(params ShapeInfo[] shapes) - { - this.Shapes = shapes; - } + public ShapeInfo[] Shapes { get; } + public ShapeInfo this[int index] => this.Shapes[index]; - public static ShapeDB LargeShapes = new - ( - // Cube - CubicShape("2eacbbf2-d8fb-4a78-91dc-7b492517ef97", x => x.AppendBox(Dims(LargeBlockSize))), - - // Slopes - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Left, Base6Directions.Up), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Right, Base6Directions.Up), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Forward, Base6Directions.Up), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Backward, Base6Directions.Up), - - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Left, Base6Directions.Down), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Right, Base6Directions.Down), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Forward, Base6Directions.Down), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Backward, Base6Directions.Down), - - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Forward, Base6Directions.Left), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Left, Base6Directions.Backward), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Backward, Base6Directions.Right), - SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Right, Base6Directions.Forward) - ); - - public static ShapeDB MidShapes = new - ( - // Cube - CubicShape("632d7385-12b9-47a6-802a-a610d0cbd1e0", x => x.AppendBox(Dims(MidBlockSize))), - - // Slopes - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Left, Base6Directions.Up), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Right, Base6Directions.Up), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Forward, Base6Directions.Up), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Backward, Base6Directions.Up), - - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Left, Base6Directions.Down), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Right, Base6Directions.Down), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Forward, Base6Directions.Down), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Backward, Base6Directions.Down), - - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Forward, Base6Directions.Left), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Left, Base6Directions.Backward), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Backward, Base6Directions.Right), - SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Right, Base6Directions.Forward) - ); - - private static ShapeInfo SlopeShape(string prefab, float size, int forward, int up) - { - var info = CubicShape + public ShapeDB(params ShapeInfo[] shapes) + { + this.Shapes = shapes; + } + + public static ShapeDB LargeShapes = new ( - prefab, - x => - { - x.AppendSlope - ( - Dims(size), - Base6Directions.Vectors[forward], - -Base6Directions.Vectors[up] - ); - } + // Cube + CubicShape("2eacbbf2-d8fb-4a78-91dc-7b492517ef97", x => x.AppendBox(Dims(LargeBlockSize))), + + // Slopes + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Left, Base6Directions.Up), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Right, Base6Directions.Up), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Forward, Base6Directions.Up), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Backward, Base6Directions.Up), + + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Left, Base6Directions.Down), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Right, Base6Directions.Down), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Forward, Base6Directions.Down), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Backward, Base6Directions.Down), + + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Forward, Base6Directions.Left), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Left, Base6Directions.Backward), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Backward, Base6Directions.Right), + SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Right, Base6Directions.Forward) ); - info.Up = up; - info.Forward = forward; - return info; - } + public static ShapeDB MidShapes = new + ( + // Cube + CubicShape("632d7385-12b9-47a6-802a-a610d0cbd1e0", x => x.AppendBox(Dims(MidBlockSize))), + + // Slopes + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Left, Base6Directions.Up), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Right, Base6Directions.Up), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Forward, Base6Directions.Up), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Backward, Base6Directions.Up), + + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Left, Base6Directions.Down), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Right, Base6Directions.Down), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Forward, Base6Directions.Down), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Backward, Base6Directions.Down), + + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Forward, Base6Directions.Left), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Left, Base6Directions.Backward), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Backward, Base6Directions.Right), + SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Right, Base6Directions.Forward) + ); - private static ShapeInfo CubicShape(string prefab, Action shape) - { - return new() + private static ShapeInfo SlopeShape(string prefab, float size, int forward, int up) { - Prefab = prefab, - Shape = MakeShape(shape) - }; - } + var info = CubicShape + ( + prefab, + x => + { + x.AppendSlope + ( + Dims(size), + Base6Directions.Vectors[forward], + -Base6Directions.Vectors[up] + ); + } + ); - private static AxisAlignedBox3f Dims(float size) - { - return new(Vector3f.Zero, size / 2); - } + info.Up = up; + info.Forward = forward; + return info; + } - private static DMesh3 MakeShape(Action factory) - { - var mesh = new DMesh3(); - factory(mesh); - return mesh; - } -} + private static ShapeInfo CubicShape(string prefab, Action shape) + { + return new() + { + Prefab = prefab, + Shape = MakeShape(shape) + }; + } -public class BlueprintMesh -{ - public const int NoContent = int.MaxValue; + private static AxisAlignedBox3f Dims(float size) + { + return new(Vector3f.Zero, size / 2); + } - public DenseGrid3i Blocks; - public ShiftGridIndexer3 Coords; - public ShapeDB Shapes; + private static DMesh3 MakeShape(Action factory) + { + var mesh = new DMesh3(); + factory(mesh); + return mesh; + } + } - public int this[Vector3i index] + public class BlueprintMesh { - get + public const int NoContent = int.MaxValue; + + public DenseGrid3i Blocks; + public ShiftGridIndexer3 Coords; + public ShapeDB Shapes; + + public int this[Vector3i index] { - if (this.Blocks.IsValidIndex(index) == false) - return NoContent; + get + { + if (this.Blocks.IsValidIndex(index) == false) + return NoContent; - return this.Blocks[index]; + return this.Blocks[index]; + } } } -} - -public class BlueprintWriter -{ - public required string WriteFolder { get; set; } - public void Write(BlueprintMesh blueprint, string name) + public class BlueprintWriter { - var sb = new StringBuilder(); - Generate(blueprint, sb); - File.WriteAllText(Path.Combine(this.WriteFolder, $"{name}.txt"), sb.ToString()); - } + public required string WriteFolder { get; set; } - public void Generate(BlueprintMesh blueprint, StringBuilder sb) - { - //Prefab|PositionX|PositionY|PositionZ|ColorHUE|ColorSATURATION|ColorVALUE|OrientationFORWARD|OrientationUP|Integrity - var blockGrid = blueprint.Blocks; - foreach (var g in blockGrid.Indices()) + public void Write(BlueprintMesh blueprint, string name) { - var content = blockGrid[g]; - if (content == BlueprintMesh.NoContent) - continue; + var sb = new StringBuilder(); + Generate(blueprint, sb); + File.WriteAllText(Path.Combine(this.WriteFolder, $"{name}.txt"), sb.ToString()); + } - if (content < 0) + public void Generate(BlueprintMesh blueprint, StringBuilder sb) + { + //Prefab|PositionX|PositionY|PositionZ|ColorHUE|ColorSATURATION|ColorVALUE|OrientationFORWARD|OrientationUP|Integrity + var blockGrid = blueprint.Blocks; + foreach (var g in blockGrid.Indices()) { - throw new NotImplementedException("Shape lists will go here"); - } + var content = blockGrid[g]; + if (content == BlueprintMesh.NoContent) + continue; - var block = blueprint.Shapes[content]; - sb.Append(block.Prefab); - sb.Append('|'); + if (content < 0) + { + throw new NotImplementedException("Shape lists will go here"); + } - var forwardAxis = block.Forward; - var upAxis = block.Up; + var block = blueprint.Shapes[content]; + sb.Append(block.Prefab); + sb.Append('|'); - var cube = blueprint.Coords.ToBox(g); - var gridPosition = ToInt(cube.Center / ShapeDB.SmallBlockSize); - gridPosition += PositionOffset - ( - //TODO: - blueprint.Shapes == ShapeDB.LargeShapes ? - new AxisAlignedBox3i(new Vector3i(-4, -4, -4), new Vector3i(5, 5, 5)) : - new AxisAlignedBox3i(new Vector3i(0, 0, 0), new Vector3i(1, 1, 1)), - forwardAxis, - upAxis - ); + var forwardAxis = block.Forward; + var upAxis = block.Up; + var cube = blueprint.Coords.ToBox(g); + var gridPosition = ToInt(cube.Center / ShapeDB.SmallBlockSize); + gridPosition += PositionOffset + ( + //TODO: + blueprint.Shapes == ShapeDB.LargeShapes ? + new AxisAlignedBox3i(new Vector3i(-4, -4, -4), new Vector3i(5, 5, 5)) : + new AxisAlignedBox3i(new Vector3i(0, 0, 0), new Vector3i(1, 1, 1)), + forwardAxis, + upAxis + ); - sb.Append(gridPosition.x); - sb.Append('|'); - sb.Append(gridPosition.y); - sb.Append('|'); - sb.Append(gridPosition.z); - sb.Append('|'); - sb.Append(0); - sb.Append('|'); - sb.Append(0); - sb.Append('|'); - sb.Append(0.25); - sb.Append('|'); + sb.Append(gridPosition.x); + sb.Append('|'); + sb.Append(gridPosition.y); + sb.Append('|'); + sb.Append(gridPosition.z); + sb.Append('|'); - sb.Append(forwardAxis); - sb.Append('|'); - sb.Append(upAxis); - sb.Append('|'); + sb.Append(0); + sb.Append('|'); + sb.Append(0); + sb.Append('|'); + sb.Append(0.25); + sb.Append('|'); - sb.Append(1); - sb.Append('|'); + sb.Append(forwardAxis); + sb.Append('|'); + sb.Append(upAxis); + sb.Append('|'); - sb.AppendLine(); - } + sb.Append(1); + sb.Append('|'); - Vector3i PositionOffset(AxisAlignedBox3i blockSize, int blockForward, int blockRight) - { - var baseRight = Base6Directions.Vectors[blockRight]; - var baseForward = -Base6Directions.Vectors[blockForward]; - return BlockOffset - ( - blockSize, - new Matrix3f + sb.AppendLine(); + } + + Vector3i PositionOffset(AxisAlignedBox3i blockSize, int blockForward, int blockRight) + { + var baseRight = Base6Directions.Vectors[blockRight]; + var baseForward = -Base6Directions.Vectors[blockForward]; + return BlockOffset ( - (Vector3f) baseRight.Cross(baseForward), - (Vector3f) baseRight, - (Vector3f) baseForward, - bRows: false - ) - ); - } + blockSize, + new Matrix3f + ( + (Vector3f)baseRight.Cross(baseForward), + (Vector3f)baseRight, + (Vector3f)baseForward, + bRows: false + ) + ); + } - static Vector3i BlockOffset(AxisAlignedBox3i blockSize, Matrix3f blockOrientation) - { - var offsetNegative = (Vector3f) blockSize.Min; - var offsetPositive = (Vector3f) blockSize.Max; + static Vector3i BlockOffset(AxisAlignedBox3i blockSize, Matrix3f blockOrientation) + { + var offsetNegative = (Vector3f)blockSize.Min; + var offsetPositive = (Vector3f)blockSize.Max; + + var a = ToInt(blockOrientation.Multiply(ref offsetNegative)); + var b = ToInt(blockOrientation.Multiply(ref offsetPositive)); - var a = ToInt(blockOrientation.Multiply(ref offsetNegative)); - var b = ToInt(blockOrientation.Multiply(ref offsetPositive)); + var minI = new Vector3i(Math.Min(a.x, b.x), Math.Min(a.y, b.y), Math.Min(a.z, b.z)); + return blockSize.Min - minI; + } - var minI = new Vector3i(Math.Min(a.x, b.x), Math.Min(a.y, b.y), Math.Min(a.z, b.z)); - return blockSize.Min - minI; + static Vector3i ToInt(Vector3d vec) + { + return new + ( + (int)Math.Round(vec.x), + (int)Math.Round(vec.y), + (int)Math.Round(vec.z) + ); + } } + } - static Vector3i ToInt(Vector3d vec) + +public static class VoxelizationKernel +{ + // GPU-safe math implementations must reside inside this class + private static int Min(int a, int b) => a < b ? a : b; + private static int Max(int a, int b) => a > b ? a : b; + private static float Min(float a, float b) => a < b ? a : b; + private static float Max(float a, float b) => a > b ? a : b; + private static float Abs(float v) => v < 0f ? -v : v; + private static float Min3(float a, float b, float c) => Min(a, Min(b, c)); + private static float Max3(float a, float b, float c) => Max(a, Max(b, c)); + private static int Floor(float val) => val < 0f ? (int)val - 1 : (int)val; + private static int Ceiling(float val) => val > (int)val ? (int)val + 1 : (int)val; + + public static void Voxelize( + Index1D index, + ArrayView triangles, + ArrayView voxelGrid, + Float3 gridOrigin, + float cellSize, + int gridX, + int gridY, + int gridZ) + { + var tri = triangles[index]; + + int minX = Max(0, Floor((tri.MinBounds.X - gridOrigin.X) / cellSize)); + int minY = Max(0, Floor((tri.MinBounds.Y - gridOrigin.Y) / cellSize)); + int minZ = Max(0, Floor((tri.MinBounds.Z - gridOrigin.Z) / cellSize)); + + int maxX = Min(gridX - 1, Ceiling((tri.MaxBounds.X - gridOrigin.X) / cellSize)); + int maxY = Min(gridY - 1, Ceiling((tri.MaxBounds.Y - gridOrigin.Y) / cellSize)); + int maxZ = Min(gridZ - 1, Ceiling((tri.MaxBounds.Z - gridOrigin.Z) / cellSize)); + + for (int z = minZ; z <= maxZ; z++) { - return new - ( - (int) Math.Round(vec.x), - (int) Math.Round(vec.y), - (int) Math.Round(vec.z) - ); + for (int y = minY; y <= maxY; y++) + { + for (int x = minX; x <= maxX; x++) + { + Float3 cellCenter = new Float3( + gridOrigin.X + (x + 0.5f) * cellSize, + gridOrigin.Y + (y + 0.5f) * cellSize, + gridOrigin.Z + (z + 0.5f) * cellSize + ); + + if (CheckTriangleBoxIntersection(tri, cellCenter, cellSize)) + { + int flatIndex = x + (y * gridX) + (z * gridX * gridY); + Atomic.Exchange(ref voxelGrid[flatIndex], 0); + } + } + } } } + + private static bool CheckTriangleBoxIntersection(GpuTriangle tri, Float3 boxCenter, float cellSize) + { + float boxHalf = cellSize * 0.5f; + + // Shift triangle to local AABB coordinate space + float v0X = tri.V0.X - boxCenter.X; float v0Y = tri.V0.Y - boxCenter.Y; float v0Z = tri.V0.Z - boxCenter.Z; + float v1X = tri.V1.X - boxCenter.X; float v1Y = tri.V1.Y - boxCenter.Y; float v1Z = tri.V1.Z - boxCenter.Z; + float v2X = tri.V2.X - boxCenter.X; float v2Y = tri.V2.Y - boxCenter.Y; float v2Z = tri.V2.Z - boxCenter.Z; + + // Compute edge vectors + float e0X = v1X - v0X; float e0Y = v1Y - v0Y; float e0Z = v1Z - v0Z; + float e1X = v2X - v1X; float e1Y = v2Y - v1Y; float e1Z = v2Z - v1Z; + float e2X = v0X - v2X; float e2Y = v0Y - v2Y; float e2Z = v0Z - v2Z; + + // SAT Test 1: Box AABB bounds + if (Min3(v0X, v1X, v2X) > boxHalf || Max3(v0X, v1X, v2X) < -boxHalf) return false; + if (Min3(v0Y, v1Y, v2Y) > boxHalf || Max3(v0Y, v1Y, v2Y) < -boxHalf) return false; + if (Min3(v0Z, v1Z, v2Z) > boxHalf || Max3(v0Z, v1Z, v2Z) < -boxHalf) return false; + + // SAT Test 2: Triangle Plane vs Box Overlap + float normalX = e0Y * e1Z - e0Z * e1Y; + float normalY = e0Z * e1X - e0X * e1Z; + float normalZ = e0X * e1Y - e0Y * e1X; + + float d = -(normalX * v0X + normalY * v0Y + normalZ * v0Z); + + float vminX = normalX > 0f ? -boxHalf : boxHalf; float vmaxX = normalX > 0f ? boxHalf : -boxHalf; + float vminY = normalY > 0f ? -boxHalf : boxHalf; float vmaxY = normalY > 0f ? boxHalf : -boxHalf; + float vminZ = normalZ > 0f ? -boxHalf : boxHalf; float vmaxZ = normalZ > 0f ? boxHalf : -boxHalf; + + if ((normalX * vminX + normalY * vminY + normalZ * vminZ) + d > 0f) return false; + if ((normalX * vmaxX + normalY * vmaxY + normalZ * vmaxZ) + d < 0f) return false; + + // SAT Test 3: Edge Cross Products + if (!AxisTest(e0Z, -e0Y, v0Y, v0Z, v2Y, v2Z, boxHalf)) return false; + if (!AxisTest(e1Z, -e1Y, v1Y, v1Z, v0Y, v0Z, boxHalf)) return false; + if (!AxisTest(e2Z, -e2Y, v2Y, v2Z, v1Y, v1Z, boxHalf)) return false; + + if (!AxisTest(-e0Z, e0X, v0X, v0Z, v2X, v2Z, boxHalf)) return false; + if (!AxisTest(-e1Z, e1X, v1X, v1Z, v0X, v0Z, boxHalf)) return false; + if (!AxisTest(-e2Z, e2X, v2X, v2Z, v1X, v1Z, boxHalf)) return false; + + if (!AxisTest(e0Y, -e0X, v0X, v0Y, v2X, v2Y, boxHalf)) return false; + if (!AxisTest(e1Y, -e1X, v1X, v1Y, v0X, v0Y, boxHalf)) return false; + if (!AxisTest(e2Y, -e2X, v2X, v2Y, v1X, v1Y, boxHalf)) return false; + + return true; + } + + private static bool AxisTest(float a, float b, float fa, float fb, float va, float vb, float boxHalf) + { + float p0 = a * fa + b * fb; + float p2 = a * va + b * vb; + float min = Min(p0, p2); + float max = Max(p0, p2); + float rad = (Abs(a) + Abs(b)) * boxHalf; + return !(min > rad || max < -rad); + } } \ No newline at end of file diff --git a/Controls/BlueprintGenerator.xaml b/Controls/BlueprintGenerator.xaml index 32dd696..0161b83 100644 --- a/Controls/BlueprintGenerator.xaml +++ b/Controls/BlueprintGenerator.xaml @@ -6,7 +6,8 @@ xmlns:local="clr-namespace:SpaceEditor.Controls" xmlns:colorPicker="clr-namespace:ColorPicker;assembly=ColorPicker" mc:Ignorable="d" - d:DesignHeight="450" d:DesignWidth="800"> + d:DesignHeight="450" d:DesignWidth="800" + PreviewKeyDown="UserControl_PreviewKeyDown"> @@ -47,10 +48,40 @@ - + + + + + update) { + // Block model modifications while a generation is running + // Prevents the accidental cancellation of a generation due to a button press in the blueprint menu + if (this.Screen.IsGenerating) return; + var model = this.Screen.Model; if (model is null) return; @@ -113,12 +118,23 @@ private void UpdateModelCopy(Action update) /// public partial class BlueprintGenerator : UserControl { + public bool IsGenerating { get; private set; } = false; + public DMesh3? Model; public AsyncLazy? ModelBVH; public CancellationTokenSource? ModelLifetime; public CancellationTokenSource? GeneratorLifetime; + private void UserControl_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == System.Windows.Input.Key.Escape) + { + this.GeneratorLifetime?.Cancel(); + e.Handled = true; + } + } + private ModelSettings ModelSettingsVM { get => (ModelSettings) this.ModelSettings.ReflectedInstance; @@ -143,6 +159,9 @@ public BlueprintGenerator() private void SelectModel(object sender, RoutedEventArgs e) { + // Block new file selection while a generation is running + if (this.IsGenerating) return; + var selectFile = new OpenFileDialog(); if (selectFile.ShowDialog(Window.GetWindow(this)) != true) return; @@ -192,12 +211,12 @@ public void SetNewModel(DMesh3? model) var modelSize = Math.Max(bb.Min.MaxAbs, bb.Max.MaxAbs) * 2; if (modelSize > 3000) { - modelInfo.AppendLine("Mode is too larget to safely convert."); + modelInfo.AppendLine("Model is too large to safely convert."); modelInfo.AppendLine("Scale it down."); } else if (modelSize > 500) { - modelInfo.AppendLine("Mode is quite large, performance issues may appear."); + modelInfo.AppendLine("Model is quite large, performance issues may appear."); modelInfo.AppendLine("Recommended to scale it down."); } @@ -220,23 +239,76 @@ public void SetNewModel(DMesh3? model) private async void GenerateBlueprint(object sender, RoutedEventArgs e) { + // Prevent accidental double-clicks from spawning multiple tasks + if (this.IsGenerating) return; + this.IsGenerating = true; + try { + // Capture the state of the Shift key at the exact moment of the click + bool isCpuFallback = System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift); + var lifetime = ResetLifetime(ref this.GeneratorLifetime, this.ModelLifetime); var model = this.Model; if (model is null) return; - + var tree = await this.ModelBVH!.Value; - var shaper = new GridShaper(model, tree); - var blueprint = shaper.Generate - ( - (GridShaper.GeneratorSettings) this.GeneratorSettings.ReflectedInstance, - lifetime - ); + var settings = (GridShaper.GeneratorSettings)this.GeneratorSettings.ReflectedInstance; + + // Hide the button and show the progress overlay + this.GenerateButton.Visibility = Visibility.Collapsed; + this.ProgressOverlay.Visibility = Visibility.Visible; + this.GenerateProgress.Value = 0; + this.ProgressText.Visibility = Visibility.Visible; // Explicitly show the text + this.ProgressText.Text = "Initializing..."; + + // Without this, the esc button dosen't work because the bluepring generation button was colapsed to make place for the progress bar + this.Focusable = true; + System.Windows.Input.Keyboard.Focus(this); + // Display the CPU/GPU indicator text + this.BlueprintDetails.Text = isCpuFallback ? "Generating via CPU (Shift fallback)..." : "Generating via GPU..."; + + // Route background progress updates back to the UI thread + var progress = new Progress<(double Value, string Message)>(p => + { + this.GenerateProgress.Value = p.Value * 100; + this.ProgressText.Text = p.Message; + }); + + BlueprintMesh blueprint; + + try + { + // Offload execution to a background thread, routing to CPU or GPU based on the flag + blueprint = await Task.Run(() => + { + if (isCpuFallback) + { + return shaper.GenerateCpu(settings, lifetime, progress); + } + else + { + return shaper.Generate(settings, lifetime, progress); + } + }, lifetime); + } + finally + { + // Guarantee the UI resets and the generate button is visible even if generation fails or is cancelled + this.GenerateButton.Visibility = Visibility.Visible; + this.GenerateProgress.Visibility = Visibility.Collapsed; + // Explicitly hide and clear the text block + this.ProgressText.Visibility = Visibility.Collapsed; + this.ProgressText.Text = string.Empty; + + this.IsGenerating = false; // Unlock the UI + } + + // The code below automatically resumes on the UI thread var gridMesh = GridMesher.Mesh(blueprint); var modelRender = CreateRenderModel(gridMesh, controlsRow: 1); lifetime.Register(() => @@ -253,16 +325,26 @@ private async void GenerateBlueprint(object sender, RoutedEventArgs e) Vector3i dimensions = GetBlueprintDiemensions(usedIndicies); string info = $"Blocks: X: {dimensions.x} Y: {dimensions.y} Z: {dimensions.z} Total: {usedIndicies.Count()}"; - this.BlueprintDetails.Text = info; + this.BlueprintDetails.Text = info; lifetime.Register(() => { this.BlueprintDetails.Text = null; }); } - catch + catch (OperationCanceledException) { - + // Silently catch the cancellation so it doesn't crash the app + this.BlueprintDetails.Text = "Generation cancelled."; + } + catch (Exception ex) + { + // Handle other general exceptions + this.BlueprintDetails.Text = "Generation failed. See logs."; + } + finally + { + this.IsGenerating = false; // Failsafe unlock } } diff --git a/SpaceEditor.csproj b/SpaceEditor.csproj index ced0c52..b6237e9 100644 --- a/SpaceEditor.csproj +++ b/SpaceEditor.csproj @@ -22,6 +22,7 @@ + @@ -50,16 +51,7 @@ $(IntermediateOutputPath)BuildInfo.g.cs - DateTime.Parse("$(BuildDateTime)") %3B - } - " - Overwrite="true" /> + @@ -71,18 +63,11 @@ - + - + - + From f1047e5687525e1121709e142786e32587d90f41 Mon Sep 17 00:00:00 2001 From: Zemogiter Date: Sun, 26 Jul 2026 02:55:30 +0200 Subject: [PATCH 2/8] Update BlueprintGenerator.xaml.cs Fix the progress bar not showing up on subsequent model conversions. --- Controls/BlueprintGenerator.xaml.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Controls/BlueprintGenerator.xaml.cs b/Controls/BlueprintGenerator.xaml.cs index e7ded5e..a764537 100644 --- a/Controls/BlueprintGenerator.xaml.cs +++ b/Controls/BlueprintGenerator.xaml.cs @@ -300,7 +300,10 @@ private async void GenerateBlueprint(object sender, RoutedEventArgs e) { // Guarantee the UI resets and the generate button is visible even if generation fails or is cancelled this.GenerateButton.Visibility = Visibility.Visible; - this.GenerateProgress.Visibility = Visibility.Collapsed; + + // Explicitly collapse the entire overlay grid, NOT just the inner progress bar + this.ProgressOverlay.Visibility = Visibility.Collapsed; + // Explicitly hide and clear the text block this.ProgressText.Visibility = Visibility.Collapsed; this.ProgressText.Text = string.Empty; From cfbcdde5e8db4cbc01d9d04b9a1d98e063446153 Mon Sep 17 00:00:00 2001 From: Zemogiter Date: Sun, 26 Jul 2026 03:12:01 +0200 Subject: [PATCH 3/8] First attempt on making the GPU rendering more consistent speed-wise Precompute triangle edges and normals outside the inner loop to avoid redundant calculations. Add cache-friendly early exit check before atomic operations to prevent unnecessary memory bus locking. Include ILGPU accelerator verification debug output. --- Algorithms/GridShaper.cs | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/Algorithms/GridShaper.cs b/Algorithms/GridShaper.cs index ed811dc..ae49d30 100644 --- a/Algorithms/GridShaper.cs +++ b/Algorithms/GridShaper.cs @@ -93,6 +93,7 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, // Initialize ILGPU Context using var context = Context.CreateDefault(); using var accelerator = context.GetPreferredDevice(preferCPU: false).CreateAccelerator(context); + System.Diagnostics.Debug.WriteLine($"\n[ILGPU VERIFICATION] Executing on: {accelerator.Name} (Type: {accelerator.AcceleratorType})\n"); // Prepare Triangle Data int triangleCount = this.Mesh.TriangleCount; @@ -673,10 +674,9 @@ static Vector3i ToInt(Vector3d vec) } } - public static class VoxelizationKernel { - // GPU-safe math implementations must reside inside this class + // GPU-safe math implementations private static int Min(int a, int b) => a < b ? a : b; private static int Max(int a, int b) => a > b ? a : b; private static float Min(float a, float b) => a < b ? a : b; @@ -707,6 +707,15 @@ public static void Voxelize( int maxY = Min(gridY - 1, Ceiling((tri.MaxBounds.Y - gridOrigin.Y) / cellSize)); int maxZ = Min(gridZ - 1, Ceiling((tri.MaxBounds.Z - gridOrigin.Z) / cellSize)); + // OPTIMIZATION 1: Precompute triangle edges and normal outside the loop + float e0X = tri.V1.X - tri.V0.X; float e0Y = tri.V1.Y - tri.V0.Y; float e0Z = tri.V1.Z - tri.V0.Z; + float e1X = tri.V2.X - tri.V1.X; float e1Y = tri.V2.Y - tri.V1.Y; float e1Z = tri.V2.Z - tri.V1.Z; + float e2X = tri.V0.X - tri.V2.X; float e2Y = tri.V0.Y - tri.V2.Y; float e2Z = tri.V0.Z - tri.V2.Z; + + float normalX = e0Y * e1Z - e0Z * e1Y; + float normalY = e0Z * e1X - e0X * e1Z; + float normalZ = e0X * e1Y - e0Y * e1X; + for (int z = minZ; z <= maxZ; z++) { for (int y = minY; y <= maxY; y++) @@ -719,17 +728,29 @@ public static void Voxelize( gridOrigin.Z + (z + 0.5f) * cellSize ); - if (CheckTriangleBoxIntersection(tri, cellCenter, cellSize)) + // Pass precomputed values into the intersection test + if (CheckTriangleBoxIntersection(tri, cellCenter, cellSize, + e0X, e0Y, e0Z, e1X, e1Y, e1Z, e2X, e2Y, e2Z, normalX, normalY, normalZ)) { int flatIndex = x + (y * gridX) + (z * gridX * gridY); - Atomic.Exchange(ref voxelGrid[flatIndex], 0); + + // OPTIMIZATION 2: Cache-friendly early exit prevents memory bus locking + if (voxelGrid[flatIndex] != 0) + { + Atomic.Exchange(ref voxelGrid[flatIndex], 0); + } } } } } } - private static bool CheckTriangleBoxIntersection(GpuTriangle tri, Float3 boxCenter, float cellSize) + private static bool CheckTriangleBoxIntersection( + GpuTriangle tri, Float3 boxCenter, float cellSize, + float e0X, float e0Y, float e0Z, + float e1X, float e1Y, float e1Z, + float e2X, float e2Y, float e2Z, + float normalX, float normalY, float normalZ) { float boxHalf = cellSize * 0.5f; @@ -738,21 +759,12 @@ private static bool CheckTriangleBoxIntersection(GpuTriangle tri, Float3 boxCent float v1X = tri.V1.X - boxCenter.X; float v1Y = tri.V1.Y - boxCenter.Y; float v1Z = tri.V1.Z - boxCenter.Z; float v2X = tri.V2.X - boxCenter.X; float v2Y = tri.V2.Y - boxCenter.Y; float v2Z = tri.V2.Z - boxCenter.Z; - // Compute edge vectors - float e0X = v1X - v0X; float e0Y = v1Y - v0Y; float e0Z = v1Z - v0Z; - float e1X = v2X - v1X; float e1Y = v2Y - v1Y; float e1Z = v2Z - v1Z; - float e2X = v0X - v2X; float e2Y = v0Y - v2Y; float e2Z = v0Z - v2Z; - // SAT Test 1: Box AABB bounds if (Min3(v0X, v1X, v2X) > boxHalf || Max3(v0X, v1X, v2X) < -boxHalf) return false; if (Min3(v0Y, v1Y, v2Y) > boxHalf || Max3(v0Y, v1Y, v2Y) < -boxHalf) return false; if (Min3(v0Z, v1Z, v2Z) > boxHalf || Max3(v0Z, v1Z, v2Z) < -boxHalf) return false; // SAT Test 2: Triangle Plane vs Box Overlap - float normalX = e0Y * e1Z - e0Z * e1Y; - float normalY = e0Z * e1X - e0X * e1Z; - float normalZ = e0X * e1Y - e0Y * e1X; - float d = -(normalX * v0X + normalY * v0Y + normalZ * v0Z); float vminX = normalX > 0f ? -boxHalf : boxHalf; float vmaxX = normalX > 0f ? boxHalf : -boxHalf; From 9e51907a6240b9848e2f99f9349c00cf9d994b59 Mon Sep 17 00:00:00 2001 From: Zemogiter Date: Sun, 26 Jul 2026 22:24:57 +0200 Subject: [PATCH 4/8] Code cleanup --- App.xaml.cs | 4 +- Controls/AsyncView.xaml.cs | 12 +-- Controls/ButtonPropertyGridControlFactory.cs | 2 +- Controls/CharacterEditor.xaml.cs | 84 +++++++++---------- Controls/ColourSlider.xaml.cs | 25 +++--- .../CompositePropertyGridControlFactory.cs | 7 +- Controls/FlatGroupingDataGridOperator.cs | 8 +- Controls/InputIdControlsFactory.cs | 12 +-- Controls/InputsEditor.xaml.cs | 13 ++- Controls/KeyBindsEditor.xaml.cs | 22 ++--- Controls/ModelViewport.xaml.cs | 14 ++-- Controls/PCUUnlocker.xaml.cs | 14 ++-- Controls/ReflectedCollection.xaml.cs | 23 ++--- Controls/ReflectedObject.xaml.cs | 12 +-- Data/GameFacts.cs | 9 +- Data/GameLinks/DbgShimResolver.cs | 5 +- Data/GameLinks/DebuggerCallbacks.cs | 76 ++++++++--------- Data/GameLinks/GameLink.Operation.cs | 24 +++--- Data/GameLinks/GameLink.cs | 23 +++-- Data/GameProxy.cs | 28 +++---- Data/PresetVM.cs | 20 ++--- Data/Settings.cs | 8 +- Data/VM.cs | 14 ++-- MainWindow.xaml.cs | 65 ++++++++++---- Rocks/AsyncLazy.cs | 7 +- Rocks/Base6Directions.cs | 3 - Rocks/CollectionRocks.cs | 9 +- Rocks/DefinitionRocks.cs | 7 +- Rocks/Disposable.cs | 6 +- Rocks/EnumerableRocks.cs | 2 +- Rocks/Enumerators.cs | 17 ++-- Rocks/MathRocks.cs | 10 +-- Rocks/MeshRocks.cs | 9 +- Rocks/NullToTemplateSelector.cs | 5 +- Rocks/ReflectionRocks.cs | 14 ++-- Rocks/StringCollectionRocks.cs | 7 +- Rocks/StringRocks.cs | 5 +- Rocks/ViewRocks.cs | 9 +- Services/VersionChecker.cs | 7 +- 39 files changed, 272 insertions(+), 369 deletions(-) diff --git a/App.xaml.cs b/App.xaml.cs index 1855bdf..f3d8e3d 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,7 +1,5 @@ -using System.Configuration; -using System.Data; +using SpaceEditor.Data; using System.Windows; -using SpaceEditor.Data; namespace SpaceEditor { diff --git a/Controls/AsyncView.xaml.cs b/Controls/AsyncView.xaml.cs index bc13aae..8e02a5c 100644 --- a/Controls/AsyncView.xaml.cs +++ b/Controls/AsyncView.xaml.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Windows; +using System.Windows; using System.Windows.Controls; namespace SpaceEditor.Controls; @@ -15,7 +11,7 @@ public AsyncView() { this.DataContextChanged += OnDataContextChanged; this.Unloaded += OnUnloaded; - + InitializeComponent(); } @@ -34,7 +30,7 @@ private async void OnDataContextChanged(object sender, DependencyPropertyChanged this.Lifetime?.Cancel(); this.Lifetime = new(); - + var lifetime = this.Lifetime.Token; this.LoadingContent.Visibility = Visibility.Visible; @@ -45,7 +41,7 @@ private async void OnDataContextChanged(object sender, DependencyPropertyChanged try { await data.WaitAsync(lifetime); - + this.LoadingContent.Visibility = Visibility.Collapsed; this.MainContent.Visibility = Visibility.Visible; } diff --git a/Controls/ButtonPropertyGridControlFactory.cs b/Controls/ButtonPropertyGridControlFactory.cs index 184dcfc..1d03dc2 100644 --- a/Controls/ButtonPropertyGridControlFactory.cs +++ b/Controls/ButtonPropertyGridControlFactory.cs @@ -15,7 +15,7 @@ public class ButtonPropertyGridControlFactory : IControlFactory { public FrameworkElement? TryCreateControl(PropertyItem property, PropertyControlFactoryOptions options) { - if (property.Descriptor.GetFirstAttributeOrDefault() is not {} target) + if (property.Descriptor.GetFirstAttributeOrDefault() is not { } target) return null; var button = new Button(); diff --git a/Controls/CharacterEditor.xaml.cs b/Controls/CharacterEditor.xaml.cs index a05e6bf..16fa259 100644 --- a/Controls/CharacterEditor.xaml.cs +++ b/Controls/CharacterEditor.xaml.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using ColorPicker.Models; +using SpaceEditor.Data; +using SpaceEditor.Data.GameLinks; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; -using ColorPicker.Models; -using SpaceEditor.Data; -using SpaceEditor.Data.GameLinks; namespace SpaceEditor.Controls; @@ -26,7 +22,7 @@ public partial class CharacterEditor : UserControl // Non-null value signifies initialization is done public volatile NotifyableColor? Color; - + private readonly GameLink GameLink; private bool IsConnectedToGameImpl; @@ -53,7 +49,7 @@ private bool IsConnectedToGame public CharacterEditor(GameProxy game, GameLink gameLink) { this.GameLink = gameLink; - + InitializeComponent(); this.IsConnectedToGame = false; @@ -65,8 +61,8 @@ public CharacterEditor(GameProxy game, GameLink gameLink) this.SecondaryColor.SecondaryColor = System.Windows.Media.Color.FromRgb ( - 0xff, - 0x2a, + 0xff, + 0x2a, 0x1f ); @@ -74,7 +70,7 @@ public CharacterEditor(GameProxy game, GameLink gameLink) { this.Albedo = Read("Resources/CharacterEditor/Albedo.png"); this.Mask = Read("Resources/CharacterEditor/Mask.png"); - this.OutputMemoryPool = (Argb32[,]) this.Albedo.Clone(); + this.OutputMemoryPool = (Argb32[,])this.Albedo.Clone(); var dpi = VisualTreeHelper.GetDpi(this); var previewBitmap = this.Dispatcher.Invoke(() => @@ -88,7 +84,7 @@ public CharacterEditor(GameProxy game, GameLink gameLink) PixelFormats.Bgra32, null ); - + Write(previewBitmap, this.Albedo); return previewBitmap; }); @@ -122,7 +118,7 @@ private void UpdatePreview() async Task Impl() { - Run: + Run: this.NeedsUpdate = false; var suitColor = new Argb32 @@ -142,22 +138,22 @@ async Task Impl() var w = albedo.GetLength(1); var h = albedo.GetLength(0); for (int x = 0; x < w; x++) - for (int y = 0; y < h; y++) - { - var c1 = albedo[y, x]; - var c2 = mask[y, x]; - - if (c2 with {A = default } != default) + for (int y = 0; y < h; y++) { - c1 = MultiplyColors(c1, MultiplyColors(c2, suitColor)); - } + var c1 = albedo[y, x]; + var c2 = mask[y, x]; - output[y, x] = c1; - } + if (c2 with { A = default } != default) + { + c1 = MultiplyColors(c1, MultiplyColors(c2, suitColor)); + } + + output[y, x] = c1; + } return output; }); - + await SendToGameIfNeeded(); Write(this.PreviewBitmap, await bitmapCompute); @@ -173,10 +169,10 @@ public static Argb32 MultiplyColors(Argb32 c1, Argb32 c2) { return new() { - B = (byte) (Math.Round((c1.R / 255.0) * (c2.R / 255.0) * 255.0)), - G = (byte) (Math.Round((c1.G / 255.0) * (c2.G / 255.0) * 255.0)), - R = (byte) (Math.Round((c1.B / 255.0) * (c2.B / 255.0) * 255.0)), - A = (byte) (Math.Round((c1.A / 255.0) * (c2.A / 255.0) * 255.0)), + B = (byte)(Math.Round((c1.R / 255.0) * (c2.R / 255.0) * 255.0)), + G = (byte)(Math.Round((c1.G / 255.0) * (c2.G / 255.0) * 255.0)), + R = (byte)(Math.Round((c1.B / 255.0) * (c2.B / 255.0) * 255.0)), + A = (byte)(Math.Round((c1.A / 255.0) * (c2.A / 255.0) * 255.0)), }; } @@ -272,10 +268,10 @@ private async void ConnectToGame(object sender, RoutedEventArgs _) this.LastConnectionTask = connectionTask; PrintStatus(success: "Testing connection ..."); - - var button = (Button) sender; + + var button = (Button)sender; button.IsEnabled = false; - + Exception? connectionError = null; try { @@ -294,7 +290,7 @@ private async void ConnectToGame(object sender, RoutedEventArgs _) // Throw if anything is stored inside await connectionTask; } - catch(Exception e) + catch (Exception e) { connectionError = e; } @@ -317,19 +313,19 @@ private async void DisconnectFromGame(object _, RoutedEventArgs __) this.LastConnectionTask = Task.CompletedTask; this.IsConnectedToGame = false; } - + private async Task SendToGameIfNeeded() { if (this.Color is null) return; - + if (this.IsConnectedToGame == false) return; - uint colorInt = ((uint) byte.MaxValue << 24) | - ((uint) (byte) (this.Color.RGB_R) << 16) | - ((uint) (byte) (this.Color.RGB_G) << 8) | - ((uint) (byte) (this.Color.RGB_B) << 0); + uint colorInt = ((uint)byte.MaxValue << 24) | + ((uint)(byte)(this.Color.RGB_R) << 16) | + ((uint)(byte)(this.Color.RGB_G) << 8) | + ((uint)(byte)(this.Color.RGB_B) << 0); var error = await Task.Run(async () => { @@ -337,7 +333,7 @@ private async Task SendToGameIfNeeded() { await this.GameLink.PaintCharacters(colorInt); } - catch(Exception e) + catch (Exception e) { return e; } @@ -351,12 +347,12 @@ private async Task SendToGameIfNeeded() private void PrintStatus(Exception? error = null, string? success = null) { var message = success; - + if (error is not null) { - message = error.Message + - Environment.NewLine + - Environment.NewLine + + message = error.Message + + Environment.NewLine + + Environment.NewLine + error.ToString(); } diff --git a/Controls/ColourSlider.xaml.cs b/Controls/ColourSlider.xaml.cs index 6c5d7bc..f47f860 100644 --- a/Controls/ColourSlider.xaml.cs +++ b/Controls/ColourSlider.xaml.cs @@ -1,8 +1,5 @@ -using System; -using System.Threading; -using System.Windows; +using System.Windows; using System.Windows.Controls; -using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -58,7 +55,7 @@ public ColourSlider() public Color SelectedColour { - get { return (Color) this.GetValue(SelectedColoursProperty); } + get { return (Color)this.GetValue(SelectedColoursProperty); } set { this.SetValue(SelectedColoursProperty, value); } } @@ -76,7 +73,7 @@ protected override void OnRender(DrawingContext drawingContext) { if (this.CacheBitmap() == false) return; - + this.SetColour(this.SelectedColour); this.isFirstTime = false; } @@ -91,11 +88,11 @@ protected override void OnValueChanged(double oldValue, double newValue) { this.isValueUpdating = true; - if (this.colourGradient is {} bitmap) + if (this.colourGradient is { } bitmap) { // work out the track position based on the control's width double width = this.colourGradient.Width; - int position = (int) (((newValue - base.Minimum) / (base.Maximum - base.Minimum)) * width); + int position = (int)(((newValue - base.Minimum) / (base.Maximum - base.Minimum)) * width); this.SelectedColour = GetColour(bitmap, position); RaiseEvent(new(ColorChangedEvent, this)); @@ -160,10 +157,10 @@ private Color GetColour(BitmapSource bitmap, int position) { if (position >= bitmap.Width - 1) { - position = (int) bitmap.Width - 2; + position = (int)bitmap.Width - 2; } - CroppedBitmap cb = new CroppedBitmap(bitmap, new Int32Rect(position, (int) this.VisualBounds.Height / 2, 1, 1)); + CroppedBitmap cb = new CroppedBitmap(bitmap, new Int32Rect(position, (int)this.VisualBounds.Height / 2, 1, 1)); byte[] tricolour = new byte[4]; cb.CopyPixels(tricolour, 4, 0); @@ -178,7 +175,7 @@ private bool CacheBitmap() if (double.IsInfinity(bounds.Width) || double.IsInfinity(bounds.Height)) return false; - RenderTargetBitmap source = new RenderTargetBitmap((int) bounds.Width, (int) bounds.Height, 96, 96, PixelFormats.Pbgra32); + RenderTargetBitmap source = new RenderTargetBitmap((int)bounds.Width, (int)bounds.Height, 96, 96, PixelFormats.Pbgra32); DrawingVisual dv = new DrawingVisual(); @@ -195,8 +192,8 @@ private bool CacheBitmap() private static void SelectedColourChangedCallBack(DependencyObject property, DependencyPropertyChangedEventArgs args) { - ColourSlider colourSlider = (ColourSlider) property; - Color colour = (Color) args.NewValue; + ColourSlider colourSlider = (ColourSlider)property; + Color colour = (Color)args.NewValue; colourSlider.SetColour(colour); } @@ -215,7 +212,7 @@ public static double Distance(Color source, Color target) public static System.Drawing.Color ToDrawingColour(Color source) { - return System.Drawing.Color.FromArgb((int) source.R, (int) source.G, (int) source.B); + return System.Drawing.Color.FromArgb((int)source.R, (int)source.G, (int)source.B); } #endregion diff --git a/Controls/CompositePropertyGridControlFactory.cs b/Controls/CompositePropertyGridControlFactory.cs index ea39a8b..7a6e397 100644 --- a/Controls/CompositePropertyGridControlFactory.cs +++ b/Controls/CompositePropertyGridControlFactory.cs @@ -1,7 +1,4 @@ using PropertyTools.Wpf; -using System; -using System.Collections.Generic; -using System.Linq; using System.Windows; namespace SpaceEditor.Controls; @@ -22,9 +19,9 @@ public override FrameworkElement CreateControl(PropertyItem property, PropertyCo public virtual FrameworkElement? TryCreateControl(PropertyItem property, PropertyControlFactoryOptions options) { - foreach(var factory in this.Factories) + foreach (var factory in this.Factories) { - if (factory.TryCreateControl(property, options) is {} control) + if (factory.TryCreateControl(property, options) is { } control) { return control; } diff --git a/Controls/FlatGroupingDataGridOperator.cs b/Controls/FlatGroupingDataGridOperator.cs index 8852f73..08e045f 100644 --- a/Controls/FlatGroupingDataGridOperator.cs +++ b/Controls/FlatGroupingDataGridOperator.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using PropertyTools.Wpf; +using PropertyTools.Wpf; namespace SpaceEditor.Controls; diff --git a/Controls/InputIdControlsFactory.cs b/Controls/InputIdControlsFactory.cs index 816f625..484baa3 100644 --- a/Controls/InputIdControlsFactory.cs +++ b/Controls/InputIdControlsFactory.cs @@ -1,9 +1,9 @@ -using System.Collections; +using PropertyTools.Wpf; +using SpaceEditor.Data; +using System.Collections; using System.Windows; using System.Windows.Controls; using System.Windows.Data; -using PropertyTools.Wpf; -using SpaceEditor.Data; namespace SpaceEditor.Controls; @@ -35,9 +35,9 @@ public class InputIdControlsFactory : IControlFactory { var inputIds = property.Descriptor.ComponentType.Name switch { - {} s when s.StartsWith("Digital") => this.InputIds.Digitals, - {} s when s.StartsWith("Analog") => this.InputIds.Analogs, - {} s when s.StartsWith("Pointer") => this.InputIds.Pointers, + { } s when s.StartsWith("Digital") => this.InputIds.Digitals, + { } s when s.StartsWith("Analog") => this.InputIds.Analogs, + { } s when s.StartsWith("Pointer") => this.InputIds.Pointers, }; var keyValueInputs = inputIds.Select(x => diff --git a/Controls/InputsEditor.xaml.cs b/Controls/InputsEditor.xaml.cs index 902ef16..d026b4c 100644 --- a/Controls/InputsEditor.xaml.cs +++ b/Controls/InputsEditor.xaml.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; +using ReflectionMagic; +using SpaceEditor.Data; using System.ComponentModel; -using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Data; -using ReflectionMagic; -using SpaceEditor.Data; namespace SpaceEditor.Controls; @@ -46,7 +43,7 @@ private void OnDataContextChanged(object sender, DependencyPropertyChangedEventA var cv = CollectionViewSource.GetDefaultView(vm.Actions.Actions.Select(x => x.Value).ToList()); cv.Filter = x => { - var candidate = ((InputActions.InputActionInfo) x).DisplayName; + var candidate = ((InputActions.InputActionInfo)x).DisplayName; return candidate.Contains(this.InputActionsSearchString, StringComparison.InvariantCultureIgnoreCase); }; @@ -62,7 +59,7 @@ private static void OnInputActionsSearchStringChanged(DependencyObject d, Depend private void OnInputActionSelected(object sender, SelectionChangedEventArgs e) { - var selected = (InputActions.InputActionInfo?) this.ActionList.SelectedValue; + var selected = (InputActions.InputActionInfo?)this.ActionList.SelectedValue; if (selected is null) goto Nothing; @@ -81,7 +78,7 @@ private void OnInputActionSelected(object sender, SelectionChangedEventArgs e) this.BindingsEditor.Visibility = Visibility.Visible; return; - Nothing: + Nothing: this.BindingsEditor.DataContext = null; this.BindingsEditor.ReflectedItems = null; this.BindingsEditor.Visibility = Visibility.Hidden; diff --git a/Controls/KeyBindsEditor.xaml.cs b/Controls/KeyBindsEditor.xaml.cs index 045f0b6..b5ffa43 100644 --- a/Controls/KeyBindsEditor.xaml.cs +++ b/Controls/KeyBindsEditor.xaml.cs @@ -1,15 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using ObservableCollections; +using SpaceEditor.Data; +using SpaceEditor.Rocks; using System.Collections.Specialized; using System.ComponentModel; using System.IO; -using System.Linq; using System.Windows; using System.Windows.Controls; -using ObservableCollections; -using SpaceEditor.Data; -using SpaceEditor.Rocks; namespace SpaceEditor.Controls; @@ -19,9 +15,9 @@ namespace SpaceEditor.Controls; public partial class KeyBindsEditor : UserControl { private const string CurrentPreset = "Current"; - + public GameProxy Game { get; } - + private Settings Settings => Settings.Default; public ObservableDictionary Presets { get; } = new(); public INotifyCollectionChanged PresetsView => this.Presets.ToNotifyCollectionChanged(); @@ -33,12 +29,12 @@ public KeyBindsEditor(GameProxy game) var mappingsFile = GameFacts.GetMappingFile(this.Game.BaseGamePath); var content = File.ReadAllText(mappingsFile); this.Presets.Add(CurrentPreset, content); - + foreach (var (key, dataString) in this.Settings.NamedPresets) { this.Presets[key] = dataString; } - + InitializeComponent(); } @@ -57,7 +53,7 @@ private void OnPresetChanged(object sender, SelectionChangedEventArgs e) private void OnPresetChanged2(object? sender, EventArgs e) { - var key = (string?) this.PresetsCombo.SelectedValue; + var key = (string?)this.PresetsCombo.SelectedValue; if (key is null) return; @@ -119,7 +115,7 @@ private void OnApplyClicked(object sender, RoutedEventArgs e) private void OnCurrentVM(Action action) { - var vm = (PresetVM?) this.InputsEditorControl.DataContext; + var vm = (PresetVM?)this.InputsEditorControl.DataContext; if (vm is null) return; diff --git a/Controls/ModelViewport.xaml.cs b/Controls/ModelViewport.xaml.cs index d56aad9..5c5bde0 100644 --- a/Controls/ModelViewport.xaml.cs +++ b/Controls/ModelViewport.xaml.cs @@ -1,9 +1,7 @@ using g4; using SpaceEditor.Data; using SpaceEditor.Rocks; -using System; using System.ComponentModel; -using System.Linq; using System.Numerics; using System.Windows; using System.Windows.Controls; @@ -33,11 +31,11 @@ public Color Color get => this.ColorImpl; set => SetField(ref this.ColorImpl, value); } - + public float Opacity { - get => (float) this.Color.A / byte.MaxValue; - set => this.Color = this.Color with {A = (byte)(value * byte.MaxValue) }; + get => (float)this.Color.A / byte.MaxValue; + set => this.Color = this.Color with { A = (byte)(value * byte.MaxValue) }; } } @@ -59,7 +57,7 @@ public IDisposable AddModel(ModelData model) ( m.Triangles().SelectMany(t => { - return new[]{ t.a, t.b, t.c }; + return new[] { t.a, t.b, t.c }; }) ); @@ -80,7 +78,7 @@ public IDisposable AddModel(ModelData model) Geometry = mesh, Transform = Transform3D.Identity }; - + var renderModel = new ModelVisual3D { Content = geometry @@ -138,7 +136,7 @@ private void OnMouseMove(object sender, MouseEventArgs e) return; var cameraPosition = this.Camera.Position; - UpdateObitCamera(default, ref cameraPosition, new(-(float) diff.X, (float) -diff.Y), 0.03f); + UpdateObitCamera(default, ref cameraPosition, new(-(float)diff.X, (float)-diff.Y), 0.03f); SetCameraParameters(cameraPosition); } diff --git a/Controls/PCUUnlocker.xaml.cs b/Controls/PCUUnlocker.xaml.cs index ab359e4..88c7051 100644 --- a/Controls/PCUUnlocker.xaml.cs +++ b/Controls/PCUUnlocker.xaml.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; +using SpaceEditor.Data; using System.IO; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; -using SpaceEditor.Data; namespace SpaceEditor.Controls; @@ -53,7 +49,7 @@ private void UnlockPCU(object sender, RoutedEventArgs e) const string PlayergridPCU = "\"PlayerGridPCU\""; const string TargetCleanupLimit = "\"TargetCleanupLimit\""; const string ExecuteCleanupLimit = "\"ExecuteCleanupLimit\""; - + var gridSectionBegin = content.IndexOf(PlayergridPCU, StringComparison.InvariantCulture); if (gridSectionBegin < 0) return null; @@ -83,12 +79,12 @@ private void UnlockPCU(object sender, RoutedEventArgs e) Settings.Default.InvokeGameAction(() => { - foreach(var (file, newContent) in contents) + foreach (var (file, newContent) in contents) { File.WriteAllText(file, newContent); } }); - + bool UpdateFile(string fileName, Func updateFunction) { var filePath = GameFacts.TryFindTargetPath(this.Game.ContentPath, [], fileName); @@ -98,7 +94,7 @@ bool UpdateFile(string fileName, Func updateFunction) } var content = contents.GetValueOrDefault(filePath) ?? File.ReadAllText(filePath); - + var newContent = updateFunction(content); if (newContent is null) return false; diff --git a/Controls/ReflectedCollection.xaml.cs b/Controls/ReflectedCollection.xaml.cs index f73877f..4716212 100644 --- a/Controls/ReflectedCollection.xaml.cs +++ b/Controls/ReflectedCollection.xaml.cs @@ -1,21 +1,8 @@ -using System; +using SpaceEditor.Rocks; using System.Collections; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -using SpaceEditor.Data; -using SpaceEditor.Rocks; namespace SpaceEditor.Controls; @@ -59,9 +46,9 @@ public ReflectedCollection() private void RemoveBinding(object sender, RoutedEventArgs e) { - var v = (FrameworkElement) sender; + var v = (FrameworkElement)sender; var current = v.DataContext!; - + UpdateCollection(x => { x.Remove(current); @@ -70,7 +57,7 @@ private void RemoveBinding(object sender, RoutedEventArgs e) private void AddElement(object sender, RoutedEventArgs e) { - var selectedType = (Type?) this.NewElementTypes.SelectedValue; + var selectedType = (Type?)this.NewElementTypes.SelectedValue; if (selectedType is null) return; @@ -84,7 +71,7 @@ private void UpdateCollection(Action update) { if (this.ReflectedItems is ICollectionView cv) { - update((IList) cv.SourceCollection); + update((IList)cv.SourceCollection); cv.Refresh(); return; } diff --git a/Controls/ReflectedObject.xaml.cs b/Controls/ReflectedObject.xaml.cs index d241d3c..3e6bfc4 100644 --- a/Controls/ReflectedObject.xaml.cs +++ b/Controls/ReflectedObject.xaml.cs @@ -1,11 +1,7 @@ -using System; +using SpaceEditor.Rocks; using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using System.Windows; using System.Windows.Controls; -using SpaceEditor.Rocks; namespace SpaceEditor.Controls; @@ -32,13 +28,13 @@ public partial class ReflectedObject : UserControl public object ReflectedInstance { - get { return (object) GetValue(ReflectedInstanceProperty); } + get { return (object)GetValue(ReflectedInstanceProperty); } set { SetValue(ReflectedInstanceProperty, value); } } public IEnumerable NewObjectTypeCandidates { - get { return (IEnumerable) GetValue(NewObjectTypeCandidatesProperty); } + get { return (IEnumerable)GetValue(NewObjectTypeCandidatesProperty); } set { SetValue(NewObjectTypeCandidatesProperty, value); } } @@ -52,7 +48,7 @@ private void OnNewTypeSelected(object sender, SelectionChangedEventArgs e) if (e.AddedItems.Count == 0) return; - var type = ((KeyValuePair) e.AddedItems[0]!).Value; + var type = ((KeyValuePair)e.AddedItems[0]!).Value; this.ReflectedInstance = type.AllocateObjectBuilder(); } } \ No newline at end of file diff --git a/Data/GameFacts.cs b/Data/GameFacts.cs index 63f7e1e..c62f005 100644 --- a/Data/GameFacts.cs +++ b/Data/GameFacts.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.IO; namespace SpaceEditor.Data; @@ -85,14 +82,14 @@ public static string? TryFindTargetPath var testDir = Path.Combine(baseDirectory, currentDirToSearchFor); if (Directory.Exists(testDir)) { - if (TryFindTargetPath(testDir, subDirectories[1..], targetFile, remainingSearchDepth) is {} result) + if (TryFindTargetPath(testDir, subDirectories[1..], targetFile, remainingSearchDepth) is { } result) return result; } } foreach (var dir in Directory.EnumerateDirectories(baseDirectory)) { - if (TryFindTargetPath(dir, subDirectories, targetFile, remainingSearchDepth) is {} result) + if (TryFindTargetPath(dir, subDirectories, targetFile, remainingSearchDepth) is { } result) return result; } diff --git a/Data/GameLinks/DbgShimResolver.cs b/Data/GameLinks/DbgShimResolver.cs index 464b6f4..0bb604e 100644 --- a/Data/GameLinks/DbgShimResolver.cs +++ b/Data/GameLinks/DbgShimResolver.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.IO; using System.Runtime.InteropServices; namespace SpaceEditor.Data.GameLinks; diff --git a/Data/GameLinks/DebuggerCallbacks.cs b/Data/GameLinks/DebuggerCallbacks.cs index 8d477f9..5e4bb0d 100644 --- a/Data/GameLinks/DebuggerCallbacks.cs +++ b/Data/GameLinks/DebuggerCallbacks.cs @@ -72,229 +72,229 @@ bool Impl(T e) if (typeof(T) == typeof(BreakpointCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnBreakpoint -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnBreakpoint -= handler; }; instance.OnBreakpoint += handler; } else if (typeof(T) == typeof(StepCompleteCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnStepComplete -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnStepComplete -= handler; }; instance.OnStepComplete += handler; } else if (typeof(T) == typeof(BreakCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnBreak -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnBreak -= handler; }; instance.OnBreak += handler; } else if (typeof(T) == typeof(ExceptionCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnException -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnException -= handler; }; instance.OnException += handler; } else if (typeof(T) == typeof(EvalCompleteCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnEvalComplete -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnEvalComplete -= handler; }; instance.OnEvalComplete += handler; } else if (typeof(T) == typeof(EvalExceptionCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnEvalException -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnEvalException -= handler; }; instance.OnEvalException += handler; } else if (typeof(T) == typeof(CreateProcessCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnCreateProcess -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnCreateProcess -= handler; }; instance.OnCreateProcess += handler; } else if (typeof(T) == typeof(ExitProcessCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnExitProcess -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnExitProcess -= handler; }; instance.OnExitProcess += handler; } else if (typeof(T) == typeof(CreateThreadCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnCreateThread -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnCreateThread -= handler; }; instance.OnCreateThread += handler; } else if (typeof(T) == typeof(ExitThreadCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnExitThread -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnExitThread -= handler; }; instance.OnExitThread += handler; } else if (typeof(T) == typeof(LoadModuleCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnLoadModule -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnLoadModule -= handler; }; instance.OnLoadModule += handler; } else if (typeof(T) == typeof(UnloadModuleCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnUnloadModule -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnUnloadModule -= handler; }; instance.OnUnloadModule += handler; } else if (typeof(T) == typeof(LoadClassCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnLoadClass -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnLoadClass -= handler; }; instance.OnLoadClass += handler; } else if (typeof(T) == typeof(UnloadClassCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnUnloadClass -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnUnloadClass -= handler; }; instance.OnUnloadClass += handler; } else if (typeof(T) == typeof(DebuggerErrorCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnDebuggerError -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnDebuggerError -= handler; }; instance.OnDebuggerError += handler; } else if (typeof(T) == typeof(LogMessageCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnLogMessage -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnLogMessage -= handler; }; instance.OnLogMessage += handler; } else if (typeof(T) == typeof(LogSwitchCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnLogSwitch -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnLogSwitch -= handler; }; instance.OnLogSwitch += handler; } else if (typeof(T) == typeof(CreateAppDomainCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnCreateAppDomain -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnCreateAppDomain -= handler; }; instance.OnCreateAppDomain += handler; } else if (typeof(T) == typeof(ExitAppDomainCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnExitAppDomain -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnExitAppDomain -= handler; }; instance.OnExitAppDomain += handler; } else if (typeof(T) == typeof(LoadAssemblyCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnLoadAssembly -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnLoadAssembly -= handler; }; instance.OnLoadAssembly += handler; } else if (typeof(T) == typeof(UnloadAssemblyCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnUnloadAssembly -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnUnloadAssembly -= handler; }; instance.OnUnloadAssembly += handler; } else if (typeof(T) == typeof(ControlCTrapCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnControlCTrap -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnControlCTrap -= handler; }; instance.OnControlCTrap += handler; } else if (typeof(T) == typeof(NameChangeCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnNameChange -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnNameChange -= handler; }; instance.OnNameChange += handler; } else if (typeof(T) == typeof(UpdateModuleSymbolsCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnUpdateModuleSymbols -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnUpdateModuleSymbols -= handler; }; instance.OnUpdateModuleSymbols += handler; } else if (typeof(T) == typeof(EditAndContinueRemapCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnEditAndContinueRemap -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnEditAndContinueRemap -= handler; }; instance.OnEditAndContinueRemap += handler; } else if (typeof(T) == typeof(BreakpointSetErrorCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnBreakpointSetError -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnBreakpointSetError -= handler; }; instance.OnBreakpointSetError += handler; } else if (typeof(T) == typeof(FunctionRemapOpportunityCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnFunctionRemapOpportunity -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnFunctionRemapOpportunity -= handler; }; instance.OnFunctionRemapOpportunity += handler; } else if (typeof(T) == typeof(CreateConnectionCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnCreateConnection -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnCreateConnection -= handler; }; instance.OnCreateConnection += handler; } else if (typeof(T) == typeof(ChangeConnectionCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnChangeConnection -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnChangeConnection -= handler; }; instance.OnChangeConnection += handler; } else if (typeof(T) == typeof(DestroyConnectionCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnDestroyConnection -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnDestroyConnection -= handler; }; instance.OnDestroyConnection += handler; } else if (typeof(T) == typeof(Exception2CorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnException2 -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnException2 -= handler; }; instance.OnException2 += handler; } else if (typeof(T) == typeof(ExceptionUnwindCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnExceptionUnwind -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnExceptionUnwind -= handler; }; instance.OnExceptionUnwind += handler; } else if (typeof(T) == typeof(FunctionRemapCompleteCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnFunctionRemapComplete -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnFunctionRemapComplete -= handler; }; instance.OnFunctionRemapComplete += handler; } else if (typeof(T) == typeof(MDANotificationCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnMDANotification -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnMDANotification -= handler; }; instance.OnMDANotification += handler; } else if (typeof(T) == typeof(CustomNotificationCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnCustomNotification -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnCustomNotification -= handler; }; instance.OnCustomNotification += handler; } else if (typeof(T) == typeof(BeforeGarbageCollectionCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnBeforeGarbageCollection -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnBeforeGarbageCollection -= handler; }; instance.OnBeforeGarbageCollection += handler; } else if (typeof(T) == typeof(AfterGarbageCollectionCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnAfterGarbageCollection -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnAfterGarbageCollection -= handler; }; instance.OnAfterGarbageCollection += handler; } else if (typeof(T) == typeof(DataBreakpointCorDebugManagedCallbackEventArgs)) { EventHandler handler = null!; - handler = (_, e) => { if (Impl((T) (object) e)) instance.OnDataBreakpoint -= handler; }; + handler = (_, e) => { if (Impl((T)(object)e)) instance.OnDataBreakpoint -= handler; }; instance.OnDataBreakpoint += handler; } diff --git a/Data/GameLinks/GameLink.Operation.cs b/Data/GameLinks/GameLink.Operation.cs index 20da939..1ff4600 100644 --- a/Data/GameLinks/GameLink.Operation.cs +++ b/Data/GameLinks/GameLink.Operation.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Runtime.CompilerServices; -using ClrDebug; +using ClrDebug; using SpaceEditor.Rocks; +using System.Runtime.CompilerServices; namespace SpaceEditor.Data.GameLinks; @@ -16,7 +12,7 @@ public class Operation public CorDebugProcess Process { get; init; } public CorDebugAppDomain Domain { get; init; } public CorDebugAssembly[] Assemblies { get; init; } - + public CorDebugManagedCallback Callbacks { get; init; } public (CorDebugModule Module, mdTypeDef Token) FindType(string typeName) @@ -49,7 +45,7 @@ public class Operation } } - Found: + Found: if (moduleMd!.TryFindTypeDefByName(typePart, parent, out parent).IsFail()) { throw new Exception($"Sub type {typePart} not found in {module!.Name}"); @@ -152,7 +148,7 @@ public async Task ExecutePreparedStep(CorDebugStepper stepper) var result = await ExecutePreparedEval(eval, expectResult: true); var handle = result as CorDebugHandleValue; - + CorDebugValue value = result; if (value is CorDebugReferenceValue ptr) { @@ -175,7 +171,7 @@ public async Task ExecutePreparedEval(CorDebugEval eval, bo var toString = FindFunction("System.Object", "ToString"); eval.CallFunction(toString.Raw, 1, [exception.Raw]); - + var extractedInfo = await Invoke(); if (extractedInfo) { @@ -208,7 +204,7 @@ public async Task ExecutePreparedEval(CorDebugEval eval, bo return null!; } - return (CorDebugHandleValue) eval.Result; + return (CorDebugHandleValue)eval.Result; async Task Invoke() { @@ -274,7 +270,7 @@ public unsafe T ReadPrimitiveValue(CorDebugValue value) } T store = default; - genericValue.GetValue((IntPtr) (void*) &store); + genericValue.GetValue((IntPtr)(void*)&store); return store; } @@ -282,8 +278,8 @@ public unsafe CorDebugValue CreatePrimitiveValue(CorDebugEval eval, T value) where T : unmanaged { var type = PrimitiveRuntimeTypeToCorType(typeof(T)); - var valueHandle = (CorDebugGenericValue) eval.CreateValue(type, null); - valueHandle.SetValue((IntPtr) (void*) &value); + var valueHandle = (CorDebugGenericValue)eval.CreateValue(type, null); + valueHandle.SetValue((IntPtr)(void*)&value); return valueHandle; } diff --git a/Data/GameLinks/GameLink.cs b/Data/GameLinks/GameLink.cs index 7884612..5d11674 100644 --- a/Data/GameLinks/GameLink.cs +++ b/Data/GameLinks/GameLink.cs @@ -1,8 +1,5 @@ using ClrDebug; -using SpaceEditor.Controls; -using System; using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; namespace SpaceEditor.Data.GameLinks; @@ -10,7 +7,7 @@ namespace SpaceEditor.Data.GameLinks; public partial class GameLink : IAsyncDisposable { public GameProxy Game { get; } - + private readonly SemaphoreSlim SyncLock = new(1, 1); private readonly DbgShim DbgShim; @@ -54,7 +51,7 @@ public async Task InvokeOperation(Func invocation) { throw new Exception($"Could not find CLR"); } - + //Version String is a comma delimited value containing dbiVersion, pidDebuggee, hmodTargetCLR var versionStr = this.DbgShim.CreateVersionStringFromModule(processId, clrs[0].Path); @@ -125,7 +122,7 @@ public Task InvokeOnMainThread(Func invocation) // Session.Update is large enough to not get inlined, but it's not guaranteed which Scene it will hit on // var engineUpdateMethod = op.FindFunction("Keen.VRage.Core.VRageCore", "Update"); // thread = await op.CatchThreadInFunction(engineUpdateMethod); - + var sessionUpdateMethod = op.FindFunction("Keen.VRage.Core.Game.Systems.Session", "Update"); thread = await op.CatchThreadInFunction(sessionUpdateMethod); @@ -163,17 +160,17 @@ public Task InvokeOnComponents(string typeName, Func var entities = op.ReadField(session, "_activeEntities"); var entityType = entities.ExactType.FirstTypeParameter; - + var toArrayMethod = op.FindFunction("System.Linq.Enumerable", "ToArray"); - + var eval = thread.CreateEval(); eval.CallParameterizedFunction(toArrayMethod.Raw, 1, [entityType.Raw], 1, [entities.Raw]); var entitiesAsArrayHandle = await op.ExecutePreparedEval(eval, expectResult: true); try { - var entitiesAsArray = (CorDebugArrayValue) entitiesAsArrayHandle.Dereference(); + var entitiesAsArray = (CorDebugArrayValue)entitiesAsArrayHandle.Dereference(); var entitiesCount = entitiesAsArray.Count; - + List componentHandles = new(); try { @@ -181,7 +178,7 @@ public Task InvokeOnComponents(string typeName, Func().Dereference(); var entityComponentsImmutableArray = op.ReadField(entity, "Components"); - var entityComponents = (CorDebugArrayValue) op.ReadField(entityComponentsImmutableArray, "array").As().Dereference(); + var entityComponents = (CorDebugArrayValue)op.ReadField(entityComponentsImmutableArray, "array").As().Dereference(); CorDebugValue? componentHit = null; var cc = entityComponents.Count; @@ -237,11 +234,11 @@ public Task PaintCharacters(uint color) var eval = thread.CreateEval(); var colorValue = op.CreatePrimitiveValue(eval, color); eval.CallFunction(fromARGBMethod.Raw, 1, [colorValue.Raw]); - + var colorHandle = await op.ExecutePreparedEval(eval, expectResult: true); try { - foreach(var characterRef in characters) + foreach (var characterRef in characters) { eval.CallFunction(suitColorSetter.Raw, 2, [characterRef.Raw, colorHandle.Raw]); await op.ExecutePreparedEval(eval); diff --git a/Data/GameProxy.cs b/Data/GameProxy.cs index 55e6c48..d8d4a6e 100644 --- a/Data/GameProxy.cs +++ b/Data/GameProxy.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using Castle.DynamicProxy; +using ReflectionMagic; +using SpaceEditor.Rocks; using System.Diagnostics; using System.IO; -using System.Linq; using System.Reflection; using System.Text; -using Castle.DynamicProxy; -using ReflectionMagic; -using SpaceEditor.Rocks; namespace SpaceEditor.Data; @@ -33,7 +29,7 @@ public class InputActionInfo public InputActionInfo? TryGetInputActionInfo(object inputActionDefinitionStub) { var id = inputActionDefinitionStub.AsDynamic().Guid; - return TryGetInputActionInfo((Guid) id); + return TryGetInputActionInfo((Guid)id); } } @@ -98,7 +94,7 @@ public class GameProxy public string BinsPath { get; } public Assembly MainAssembly { get; } - + public AsyncLazy InputIds { get; } public AsyncLazy InputActions { get; } @@ -111,10 +107,10 @@ public GameProxy(string baseGamePath) var se2 = ReflectionRocks.GetLib(this.BinsPath, GameFacts.MainDll); this.MainAssembly = se2; - var st = FindType("Keen.VRage.Library.Utils.Singleton"); + var st = FindType("Keen.VRage.Library.Utils.Singleton"); var mdt = FindType("Keen.VRage.Library.Reflection.MetadataManager"); var md = st.AsDynamicType().GetInstance(mdt); - md.PushContext(new[]{se2}); + md.PushContext(new[] { se2 }); this.InputIds = new(LoadInputIds); this.InputActions = new(LoadInputActions); @@ -139,7 +135,7 @@ public dynamic DeserializeObject(Stream content, params object[] services) var format = Enum.Parse(FindType("SerializerFormat"), "Json"); - using var sc = (IDisposable) Activator.CreateInstance(FindType("SerializationContext"), content, "NoName.txt", typedServices)!; + using var sc = (IDisposable)Activator.CreateInstance(FindType("SerializationContext"), content, "NoName.txt", typedServices)!; return FindType("SerializationHelper").AsDynamicType().DeserializeAbstract(sc, format); } @@ -151,7 +147,7 @@ public string SerializeObject(object instance, params object[] services) var format = Enum.Parse(FindType("SerializerFormat"), "Json"); using var data = new MemoryStream(); - using var sc = (IDisposable) Activator.CreateInstance(FindType("SerializationContext"), data, "NoName.txt", typedServices)!; + using var sc = (IDisposable)Activator.CreateInstance(FindType("SerializationContext"), data, "NoName.txt", typedServices)!; FindType("SerializationHelper").GetMethod("SerializeAbstract")!.MakeGenericMethod(typeof(object)).Invoke(null, [sc, instance, format]); return Encoding.UTF8.GetString(data.GetBuffer().AsSpan()[..(int)data.Length]); @@ -257,11 +253,11 @@ public void Intercept(IInvocation invocation) if (methodName == "TryLocateDefinition" && invocation.Arguments.Length == 3) { - var id = (Guid) invocation.Arguments[0]!; - var type = (Type) invocation.Arguments[1]!; + var id = (Guid)invocation.Arguments[0]!; + var type = (Type)invocation.Arguments[1]!; invocation.Arguments[2] = this.Actions.TryGetInputActionInfo(id)?.DefinitionInstanceStub ?? DefinitionRocks.AllocateDefinitionStub(type, id); - + invocation.ReturnValue = true; } else diff --git a/Data/PresetVM.cs b/Data/PresetVM.cs index 313b6c3..90fe54a 100644 --- a/Data/PresetVM.cs +++ b/Data/PresetVM.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using ReflectionMagic; +using ReflectionMagic; using SpaceEditor.Rocks; +using System.Collections; namespace SpaceEditor.Data; @@ -24,7 +20,7 @@ public string Key public string DataString { get; private set; } public object RootObject { get; private set; } public IDictionary Bindings { get; private set; } - + public PresetVM(GameProxy game) { this.Game = game; @@ -35,11 +31,11 @@ public PresetVM(GameProxy game) var def = this.Actions.TryGetInputActionInfo(id)?.DefinitionInstanceStub; if (def is null) return null; - + if (this.Bindings.Contains(def) == false) return null; - - return (IList) this.Bindings[def]!; + + return (IList)this.Bindings[def]!; } public void LoadDataString(string content) @@ -49,9 +45,9 @@ public void LoadDataString(string content) this.DataString = content; this.RootObject = DynamicHelper.Unwrap(mappings); - this.Bindings = (IDictionary) DynamicHelper.Unwrap(mappings.ControlsPerAction); + this.Bindings = (IDictionary)DynamicHelper.Unwrap(mappings.ControlsPerAction); } - + public string ToDataString() { return this.Game.SerializeObject(this.RootObject); diff --git a/Data/Settings.cs b/Data/Settings.cs index d75b5e7..ce835da 100644 --- a/Data/Settings.cs +++ b/Data/Settings.cs @@ -1,6 +1,6 @@ -using System.Text; +using SpaceEditor.Rocks; +using System.Text; using System.Windows; -using SpaceEditor.Rocks; namespace SpaceEditor.Data; @@ -21,12 +21,12 @@ public Settings() // this.SettingsSaving += this.SettingsSavingEventHandler; // } - + private void SettingChangingEventHandler(object sender, System.Configuration.SettingChangingEventArgs e) { // Add code to handle the SettingChangingEvent event here. } - + private void SettingsSavingEventHandler(object sender, System.ComponentModel.CancelEventArgs e) { // Add code to handle the SettingsSaving event here. diff --git a/Data/VM.cs b/Data/VM.cs index 94bf2ae..db858d0 100644 --- a/Data/VM.cs +++ b/Data/VM.cs @@ -1,19 +1,19 @@ -using System.ComponentModel; +using SpaceEditor.Rocks; +using System.ComponentModel; using System.Runtime.CompilerServices; -using SpaceEditor.Rocks; namespace SpaceEditor.Data; public abstract class VM : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; - + public IDisposable Bind(string property, Action consumer) { var source = GetType().GetProperty(property)!.GetMethod; consumer(Getvalue()); - + PropertyChangedEventHandler handler = (sender, args) => { consumer(Getvalue()); @@ -23,13 +23,13 @@ public IDisposable Bind(string property, Action consumer) { this.PropertyChanged -= handler; }); - + T Getvalue() { - return (T) source.Invoke(this, null); + return (T)source.Invoke(this, null); } } - + protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index ade1cf1..d13a754 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -1,5 +1,8 @@ -using System; -using System.Collections; +using Microsoft.Win32; +using SpaceEditor.Controls; +using SpaceEditor.Data; +using SpaceEditor.Data.GameLinks; +using SpaceEditor.Services; using System.ComponentModel; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -7,21 +10,19 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Navigation; -using Microsoft.Win32; -using SpaceEditor.Controls; -using SpaceEditor.Data; -using SpaceEditor.Data.GameLinks; -using SpaceEditor.Services; namespace SpaceEditor; public partial class MainWindow : Window, INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; - + private Settings Settings => Settings.Default; private GameLink? GameLink; + // Token to ensure overlapping reloads are safely aborted + private CancellationTokenSource? _reloadCts; + public string GamePath { get => this.Settings.GamePath; @@ -44,7 +45,7 @@ public MainWindow() try { var latest = await VersionChecker.GetLatestVersionInfo(); - if (latest.Published.Date != BuildInfo.BuildTimeUtc.Date) + if (latest.Published.Date > BuildInfo.BuildTimeUtc.Date) { this.UpdateHints.Visibility = Visibility.Visible; } @@ -56,6 +57,11 @@ public MainWindow() private async void Reload(object sender, RoutedEventArgs e) { + // Abort any currently running background initialization + _reloadCts?.Cancel(); + _reloadCts = new CancellationTokenSource(); + var token = _reloadCts.Token; + var tabs = this.MainTabs.Items; while (tabs.Count > 1) { @@ -68,17 +74,36 @@ private async void Reload(object sender, RoutedEventArgs e) return; } - this.InfoText.Text = "Loading ..."; + this.InfoText.Text = "Loading Engine and Game Data in background... (This may take a moment)"; + try { + // Offload directory searching and math to the background thread var game = await Task.Run(async () => { - var game = new GameProxy(this.GamePath); - _ = await game.InputActions; - _ = await game.InputIds; - return game; - }); + // 1. Initialize GameProxy (Searches directory, loads assemblies) + var proxy = new GameProxy(this.GamePath); + _ = await proxy.InputActions; + _ = await proxy.InputIds; + + token.ThrowIfCancellationRequested(); + + // 2. Pre-warm the Blueprint Generator's heavy static meshes in the background. + _ = SpaceEditor.Algorithms.ShapeDB.LargeShapes; + _ = SpaceEditor.Algorithms.ShapeDB.MidShapes; + + // 3. Pre-compile the ILGPU Kernel in the background + _ = SpaceEditor.Algorithms.GridShaper.GpuSetup.Accelerator; + + return proxy; + }, token); + // Double check cancellation before touching the main thread engine or UI + if (token.IsCancellationRequested) return; + + // 3. Initialize the VRage Engine connection ON THE MAIN THREAD. + // Game engines crash (Exit Code 1) if initialized on a background thread. + var newGameLink = new GameLink(game); var propertyGridFactoryKey = "CompositePropertyGridControlFactory"; this.Resources.Remove(propertyGridFactoryKey); this.Resources.Add(propertyGridFactoryKey, new CompositePropertyGridControlFactory @@ -110,7 +135,7 @@ private async void Reload(object sender, RoutedEventArgs e) await this.GameLink.DisposeAsync(); } - this.GameLink = new GameLink(game); + this.GameLink = newGameLink; tabs.Add(new TabItem { @@ -127,7 +152,7 @@ private async void Reload(object sender, RoutedEventArgs e) var sb = new StringBuilder(); sb.AppendLine("Loading finished"); sb.AppendLine(); - + sb.AppendLine("Main Assembly:"); var gameExe = game.MainAssembly; sb.AppendLine($"{gameExe.GetName().Name}"); @@ -136,9 +161,13 @@ private async void Reload(object sender, RoutedEventArgs e) sb.AppendLine(); sb.AppendLine("Use Tabs above to access individual features"); - + this.InfoText.Text = sb.ToString(); } + catch (OperationCanceledException) + { + // Silently handle task cancellation if the user changes the path rapidly + } catch (Exception ex) { this.InfoText.Text = "Exception happened during initial loading:" + Environment.NewLine + ex; diff --git a/Rocks/AsyncLazy.cs b/Rocks/AsyncLazy.cs index 2a5f327..54a5876 100644 --- a/Rocks/AsyncLazy.cs +++ b/Rocks/AsyncLazy.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.CompilerServices; namespace SpaceEditor.Rocks; diff --git a/Rocks/Base6Directions.cs b/Rocks/Base6Directions.cs index 356c044..db5ece6 100644 --- a/Rocks/Base6Directions.cs +++ b/Rocks/Base6Directions.cs @@ -1,7 +1,4 @@ using g4; -using System; -using System.Collections.Generic; -using System.Linq; namespace SpaceEditor.Rocks; diff --git a/Rocks/CollectionRocks.cs b/Rocks/CollectionRocks.cs index acc056a..9f5f118 100644 --- a/Rocks/CollectionRocks.cs +++ b/Rocks/CollectionRocks.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace SpaceEditor.Rocks; @@ -11,12 +8,12 @@ public static TValue GetOrAdd ( this Dictionary dictionary, TKey key, - Funcfactory + Func factory ) where TKey : notnull { ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out var existed); - + if (existed == false) { value = factory(key); diff --git a/Rocks/DefinitionRocks.cs b/Rocks/DefinitionRocks.cs index e9ff2a1..aa0ea3b 100644 --- a/Rocks/DefinitionRocks.cs +++ b/Rocks/DefinitionRocks.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using ReflectionMagic; +using ReflectionMagic; namespace SpaceEditor.Rocks; diff --git a/Rocks/Disposable.cs b/Rocks/Disposable.cs index a215381..ef3137c 100644 --- a/Rocks/Disposable.cs +++ b/Rocks/Disposable.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace SpaceEditor.Rocks; +namespace SpaceEditor.Rocks; public class Disposable : IDisposable { diff --git a/Rocks/EnumerableRocks.cs b/Rocks/EnumerableRocks.cs index 58bcd7e..301533e 100644 --- a/Rocks/EnumerableRocks.cs +++ b/Rocks/EnumerableRocks.cs @@ -30,7 +30,7 @@ public static class EnumerableRocks first = false; current = enumerator.Current; } - + yield return (current, first, Last: true); } diff --git a/Rocks/Enumerators.cs b/Rocks/Enumerators.cs index a5d4918..47cbb61 100644 --- a/Rocks/Enumerators.cs +++ b/Rocks/Enumerators.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using g4; +using g4; namespace SpaceEditor.Rocks; @@ -24,10 +19,10 @@ public static IEnumerable BoxRange(AxisAlignedBox3d box, ShiftGridInde public static IEnumerable BoxRange(Vector3i minInclusive, Vector3i maxInclusive) { for (int z = minInclusive.z; z <= maxInclusive.z; ++z) - for (int y = minInclusive.y; y <= maxInclusive.y; ++y) - for (int x = minInclusive.x; x <= maxInclusive.x; ++x) - { - yield return new(x, y, z); - } + for (int y = minInclusive.y; y <= maxInclusive.y; ++y) + for (int x = minInclusive.x; x <= maxInclusive.x; ++x) + { + yield return new(x, y, z); + } } } \ No newline at end of file diff --git a/Rocks/MathRocks.cs b/Rocks/MathRocks.cs index f55efd3..9f9e2cd 100644 --- a/Rocks/MathRocks.cs +++ b/Rocks/MathRocks.cs @@ -1,8 +1,4 @@ using g4; -using System; -using System.Buffers.Text; -using System.Collections.Generic; -using System.Linq; namespace SpaceEditor.Rocks; @@ -41,8 +37,8 @@ public static Frame3f ForwardUpTranslate(int forward, int up, Vector3f translate { return ForwardUpTranslate ( - (Vector3f) Base6Directions.Vectors[forward], - (Vector3f) Base6Directions.Vectors[up], + (Vector3f)Base6Directions.Vectors[forward], + (Vector3f)Base6Directions.Vectors[up], translate ); } @@ -118,7 +114,7 @@ public static IntersectResult IntersectWithTriangle(this AxisAlignedBox3d box, T { return BoxTriangleIntersection.IntersectBoxWithTriangle(box, tri); } - + public static Bitmap3 Clone(this Bitmap3 value) { var copy = new Bitmap3(value.Dimensions); diff --git a/Rocks/MeshRocks.cs b/Rocks/MeshRocks.cs index 1149074..acb79b2 100644 --- a/Rocks/MeshRocks.cs +++ b/Rocks/MeshRocks.cs @@ -1,7 +1,4 @@ using g4; -using System; -using System.Collections.Generic; -using System.Linq; namespace SpaceEditor.Rocks; @@ -9,7 +6,7 @@ public static class MeshRocks { public static IEnumerable EnumerateTriangles(this DMesh3 mesh) { - foreach(var triangle in mesh.Triangles()) + foreach (var triangle in mesh.Triangles()) { yield return new ( @@ -41,7 +38,7 @@ public static void AppendMesh(this DMesh3 target, DMesh3 source, Frame3f transfo var editor = new MeshEditor(target); editor.AppendMesh(source, out var newVertices); - foreach(var vertexId in newVertices) + foreach (var vertexId in newVertices) { var position = target.GetVertex(vertexId); //position = transform.Multiply(ref position); @@ -126,7 +123,7 @@ public static void AppendSlope(this DMesh3 mesh, AxisAlignedBox3d box, Vector3i var vertex = rotation.Multiply(ref vertices[vertexIndex]) * scale + origin; Vector3d normalD = normal; - normal = (Vector3f) rotation.Multiply(ref normalD); + normal = (Vector3f)rotation.Multiply(ref normalD); indices[i] = mesh.AppendVertex(new NewVertexInfo(vertex, normal)); } diff --git a/Rocks/NullToTemplateSelector.cs b/Rocks/NullToTemplateSelector.cs index 0bd84d8..855aebd 100644 --- a/Rocks/NullToTemplateSelector.cs +++ b/Rocks/NullToTemplateSelector.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Windows; using System.Windows.Controls; -using System.Windows; namespace SpaceEditor.Rocks; diff --git a/Rocks/ReflectionRocks.cs b/Rocks/ReflectionRocks.cs index 817398d..bb71967 100644 --- a/Rocks/ReflectionRocks.cs +++ b/Rocks/ReflectionRocks.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections; using System.IO; using System.Reflection; @@ -10,9 +8,9 @@ public static class ReflectionRocks { public static Type? TryFindType(string assemblyHintPath, ReadOnlySpan probeAssemblies, string typeName) { - foreach(var assembly in probeAssemblies) + foreach (var assembly in probeAssemblies) { - if (GetLib(assemblyHintPath, assembly).TryFindType(typeName) is {} foundType) + if (GetLib(assemblyHintPath, assembly).TryFindType(typeName) is { } foundType) return foundType; } @@ -59,7 +57,7 @@ public static IEnumerable TryFindDerives(this Assembly assembly, Type base public static Assembly GetLib(string hintPath, string assembly) { var app = AppDomain.CurrentDomain; - if (TryGetPreloadedAssembly(assembly) is {} preloaded) + if (TryGetPreloadedAssembly(assembly) is { } preloaded) { return preloaded; } @@ -67,7 +65,7 @@ public static Assembly GetLib(string hintPath, string assembly) app.AssemblyResolve += (_, args) => { var requestedName = new AssemblyName(args.Name).Name; - if (TryGetPreloadedAssembly(requestedName) is {} preloaded) + if (TryGetPreloadedAssembly(requestedName) is { } preloaded) { return preloaded; } @@ -112,7 +110,7 @@ public static IEnumerable GetInstanceMembers(this Type declaringType public static object AllocateObjectBuilder(this Type obType) { var instance = Activator.CreateInstance(obType)!; - + foreach (var field in obType.GetInstanceMembers().OfType()) { if (typeof(ICollection).IsAssignableFrom(field.FieldType) == false) diff --git a/Rocks/StringCollectionRocks.cs b/Rocks/StringCollectionRocks.cs index b45e7b3..7511e0c 100644 --- a/Rocks/StringCollectionRocks.cs +++ b/Rocks/StringCollectionRocks.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; +using System.Collections.Specialized; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SpaceEditor.Rocks; diff --git a/Rocks/StringRocks.cs b/Rocks/StringRocks.cs index 71d9830..792b10b 100644 --- a/Rocks/StringRocks.cs +++ b/Rocks/StringRocks.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.IO; using System.Text; namespace SpaceEditor.Rocks; diff --git a/Rocks/ViewRocks.cs b/Rocks/ViewRocks.cs index ddae446..ee04dc0 100644 --- a/Rocks/ViewRocks.cs +++ b/Rocks/ViewRocks.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; +using System.Windows; using System.Windows.Media; namespace SpaceEditor.Rocks; @@ -18,7 +13,7 @@ public static class ViewRocks if (current is T found) return found; - current = (FrameworkElement?) VisualTreeHelper.GetParent(current); + current = (FrameworkElement?)VisualTreeHelper.GetParent(current); } diff --git a/Services/VersionChecker.cs b/Services/VersionChecker.cs index e9f1349..aad7e36 100644 --- a/Services/VersionChecker.cs +++ b/Services/VersionChecker.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Text.Json; namespace SpaceEditor.Services; @@ -21,7 +18,7 @@ public static async Task GetLatestVersionInfo() var queryURL = $"https://api.github.com/repos/InflexCZE/SpaceEditor/releases/latest"; var latestReleaseJSON = await DownloadStringAsync(queryURL).ConfigureAwait(false); - + var data = JsonDocument.Parse(latestReleaseJSON).RootElement; var assets = data.GetProperty("assets").EnumerateArray(); return new() From fc2ee8b9b67ca81cc74cf4401c9fd155947b7bd5 Mon Sep 17 00:00:00 2001 From: Zemogiter Date: Sun, 26 Jul 2026 22:57:30 +0200 Subject: [PATCH 5/8] Performance optimization: GPU caching and parallel processing Major performance improvements across grid generation and serialization: - Add GpuSetup static class to cache ILGPU context and kernel globally, eliminating repeated initialization overhead - Parallelize grid reconstruction and slope evaluation passes instead of sequential iteration - Track active (filled) blocks to skip empty cells during slope evaluation, reducing loop iterations by orders of magnitude - Pre-cache reflection types in GameProxy to eliminate repeated FindType() lookups during serialization - Optimize FileStream creation with proper FileAccess/FileShare flags - Use dedicated lock object in LoadInputActions to prevent thread contention - Add stopwatch timing and debug output - Clean up unused imports and fix namespace references in MainWindow --- Algorithms/GridShaper.cs | 388 +++++++++++++--------------- Controls/BlueprintGenerator.xaml.cs | 54 ++-- Data/GameProxy.cs | 48 ++-- MainWindow.xaml | 2 +- MainWindow.xaml.cs | 6 +- 5 files changed, 248 insertions(+), 250 deletions(-) diff --git a/Algorithms/GridShaper.cs b/Algorithms/GridShaper.cs index ae49d30..09c2a54 100644 --- a/Algorithms/GridShaper.cs +++ b/Algorithms/GridShaper.cs @@ -1,16 +1,9 @@ -using Assimp; -using g4; +using g4; using ILGPU; using ILGPU.Runtime; using PropertyTools.DataAnnotations; -using SpaceEditor.Algorithms; using SpaceEditor.Rocks; -using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; -using System.Numerics; using System.Text; namespace SpaceEditor.Algorithms; @@ -43,10 +36,29 @@ public GridShaper(DMesh3 mesh, DMeshAABBTree3 tree) this.Tree = tree; } + // Class to cache the GPU context and kernel globally + public static class GpuSetup + { + public static Context Context { get; } + public static Accelerator Accelerator { get; } + public static Action, ArrayView, Float3, float, int, int, int> VoxelizeKernel { get; } + + static GpuSetup() + { + Context = Context.CreateDefault(); + Accelerator = Context.GetPreferredDevice(preferCPU: false).CreateAccelerator(Context); + System.Diagnostics.Debug.WriteLine($"\n[ILGPU INITIALIZATION] Compiled and Cached on: {Accelerator.Name} (Type: {Accelerator.AcceleratorType})\n"); + + VoxelizeKernel = Accelerator.LoadAutoGroupedStreamKernel< + Index1D, ArrayView, ArrayView, Float3, float, int, int, int>( + VoxelizationKernel.Voxelize); + } + } + public static class BlockSizes { public const string TwoPointFive = "2.5m"; - public const string HalfMeter = "0.5m (VERY VERY slow on large ships!)"; + public const string HalfMeter = "0.5m (May be slow on large ships!)"; public const string TwentyFiveC = nameof(TwentyFiveC); } @@ -90,9 +102,9 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, var cellCount = (int)Math.Ceiling(boundingBox.MaxDim / blockSize); var indexer = new ShiftGridIndexer3(boundingBox.Min, blockSize); - // Initialize ILGPU Context - using var context = Context.CreateDefault(); - using var accelerator = context.GetPreferredDevice(preferCPU: false).CreateAccelerator(context); + // Access the cached GPU environment instead of creating a new one + var accelerator = GpuSetup.Accelerator; + var voxelizeKernel = GpuSetup.VoxelizeKernel; System.Diagnostics.Debug.WriteLine($"\n[ILGPU VERIFICATION] Executing on: {accelerator.Name} (Type: {accelerator.AcceleratorType})\n"); // Prepare Triangle Data @@ -104,7 +116,6 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, foreach (var triangle in this.Mesh.EnumerateTriangles()) { ct.ThrowIfCancellationRequested(); - // Report progress periodically to avoid UI thread spam if (tIndex % 5000 == 0) progress?.Report((0.1 * ((double)tIndex / triangleCount), "Mesh Flattening...")); var box = triangle.ToBox(); @@ -118,23 +129,14 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, }; } - // Allocate GPU Memory using var deviceTriangles = accelerator.Allocate1D(flatTriangles); - - // Flat 1D representation of the 3D voxel grid int totalCells = cellCount * cellCount * cellCount; int[] initialGrid = new int[totalCells]; Array.Fill(initialGrid, BlueprintMesh.NoContent); using var deviceGrid = accelerator.Allocate1D(initialGrid); - // Load and compile kernel - var voxelizeKernel = accelerator.LoadAutoGroupedStreamKernel< - Index1D, ArrayView, ArrayView, Float3, float, int, int, int>( - VoxelizationKernel.Voxelize); - Float3 origin = new Float3((float)boundingBox.Min.x, (float)boundingBox.Min.y, (float)boundingBox.Min.z); - // Dispatch execution to the GPU voxelizeKernel( deviceTriangles.IntExtent, deviceTriangles.View, @@ -151,15 +153,13 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, progress?.Report((0.40, "Voxelization...")); var flatResults = deviceGrid.GetAsArray1D(); - // Reconstruct the internal BlueprintMesh data structure var bmp = new DenseGrid3i(cellCount, cellCount, cellCount, BlueprintMesh.NoContent); // PHASE 3: Grid Reconstruction (40% - 70%) - for (int z = 0; z < cellCount; z++) + int processedZ = 0; + var activeBlocks = new System.Collections.Concurrent.ConcurrentBag(); + Parallel.For(0, cellCount, new ParallelOptions { CancellationToken = ct }, z => { - ct.ThrowIfCancellationRequested(); - if (z % 10 == 0) progress?.Report((0.40 + (0.30 * ((double)z / cellCount)), "Grid Reconstruction...")); - for (int y = 0; y < cellCount; y++) { for (int x = 0; x < cellCount; x++) @@ -167,11 +167,19 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, int flatIdx = x + (y * cellCount) + (z * cellCount * cellCount); if (flatResults[flatIdx] == 0) { - bmp[new g4.Vector3i(x, y, z)] = 0; + var cell = new g4.Vector3i(x, y, z); + bmp[cell] = 0; + activeBlocks.Add(cell); } } } - } + + int currentZ = Interlocked.Increment(ref processedZ); + if (currentZ % 10 == 0) + { + progress?.Report((0.40 + (0.30 * ((double)currentZ / cellCount)), "Grid Reconstruction...")); + } + }); var blueprint = new BlueprintMesh(); blueprint.Blocks = bmp; @@ -183,33 +191,13 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, }; // PHASE 4: Slope Generation (70% - 100%) - progress?.Report((0.70, "Slope Evaluation...")); + progress?.Report((0.70, "Slope Evaluation...")); int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); int executedPasses = 0; - if (settings.SlopesUpper) - { - ExecSlopes(1); - ExecSlopes(2); - ExecSlopes(3); - ExecSlopes(4); - } - - if (settings.SlopesLower) - { - ExecSlopes(5); - ExecSlopes(6); - ExecSlopes(7); - ExecSlopes(8); - } - - if (settings.SlopesSides) - { - ExecSlopes(9); - ExecSlopes(10); - ExecSlopes(11); - ExecSlopes(12); - } + if (settings.SlopesUpper) { ExecSlopes(1); ExecSlopes(2); ExecSlopes(3); ExecSlopes(4); } + if (settings.SlopesLower) { ExecSlopes(5); ExecSlopes(6); ExecSlopes(7); ExecSlopes(8); } + if (settings.SlopesSides) { ExecSlopes(9); ExecSlopes(10); ExecSlopes(11); ExecSlopes(12); } void ExecSlopes(int content) { @@ -219,42 +207,38 @@ void ExecSlopes(int content) var supportDirectionA = -probeDirectionA; var supportDirectionB = -probeDirectionB; - foreach (var g in bmp.Indices()) + // Multithreaded Slope Evaluation strictly over filled blocks + System.Threading.Tasks.Parallel.ForEach(activeBlocks, new System.Threading.Tasks.ParallelOptions { CancellationToken = ct }, g => { - ct.ThrowIfCancellationRequested(); + if (blueprint[g] != 0) return; // 'return' breaks out of the lambda for this specific block - if (blueprint[g] != 0) continue; - - if(blueprint[g + probeDirectionA] != BlueprintMesh.NoContent ||blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) + if (blueprint[g + probeDirectionA] != BlueprintMesh.NoContent || blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) { - continue; + return; } if (settings.SlopesMustBeSupported) { - ct.ThrowIfCancellationRequested(); - if(blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) + if (blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) { - continue; + return; } } bmp[g] = content; - } - } + }); - // Report progress after each directional pass completes - if (totalSlopePasses > 0) - { - executedPasses++; - progress?.Report((0.70 + (0.30 * ((double)executedPasses / totalSlopePasses)), "Slope Evaluation...")); + if (totalSlopePasses > 0) + { + int currentPass = System.Threading.Interlocked.Increment(ref executedPasses); + progress?.Report((0.70 + (0.30 * ((double)currentPass / totalSlopePasses)), "Slope Evaluation...")); + } } - progress?.Report((1.0, "Complete!")); + progress?.Report((1.0, "Complete, finalization!")); return blueprint; } - //Old, CPU based rendering. Kept here as a legacy option, in case using the GPU is not feasible for some reason. It is significantly slower than the GPU version, especially for large meshes. public BlueprintMesh GenerateCpu(GeneratorSettings settings, CancellationToken ct, IProgress<(double, string)> progress = null) { var blockSize = settings.BlockSize switch @@ -266,18 +250,19 @@ public BlueprintMesh GenerateCpu(GeneratorSettings settings, CancellationToken c var minimalBounds = this.Tree.Bounds; minimalBounds.Min -= blockSize; minimalBounds.Max += blockSize; - + var boundingBox = new AxisAlignedBox3d(new Vector3d(0), blockSize / 2); while (boundingBox.Contains(minimalBounds) == false) { boundingBox.Scale(2, 2, 2); } - var cellCount = (int) Math.Ceiling(boundingBox.MaxDim / blockSize); + System.Diagnostics.Debug.WriteLine($"\nExecuting the model to blueprint conversion on the CPU\n"); + var cellCount = (int)Math.Ceiling(boundingBox.MaxDim / blockSize); var indexer = new ShiftGridIndexer3(boundingBox.Min, blockSize); var bmp = new DenseGrid3i(cellCount, cellCount, cellCount, BlueprintMesh.NoContent); - + var blueprint = new BlueprintMesh(); blueprint.Blocks = bmp; blueprint.Coords = indexer; @@ -307,33 +292,23 @@ public BlueprintMesh GenerateCpu(GeneratorSettings settings, CancellationToken c } } - progress?.Report((0.70, "Dispatch & Execution...")); - int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); - int executedPasses = 0; - - if (settings.SlopesUpper) + // OPTIMIZATION: Gather only the active blocks to eliminate millions of empty-space checks + var activeBlocks = new List(); + foreach (var g in bmp.Indices()) { - ExecSlopes(1); - ExecSlopes(2); - ExecSlopes(3); - ExecSlopes(4); + if (bmp[g] == 0) + { + activeBlocks.Add(g); + } } - if (settings.SlopesLower) - { - ExecSlopes(5); - ExecSlopes(6); - ExecSlopes(7); - ExecSlopes(8); - } + progress?.Report((0.70, "Slope Evaluation...")); + int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); + int executedPasses = 0; - if (settings.SlopesSides) - { - ExecSlopes(9); - ExecSlopes(10); - ExecSlopes(11); - ExecSlopes(12); - } + if (settings.SlopesUpper) { ExecSlopes(1); ExecSlopes(2); ExecSlopes(3); ExecSlopes(4); } + if (settings.SlopesLower) { ExecSlopes(5); ExecSlopes(6); ExecSlopes(7); ExecSlopes(8); } + if (settings.SlopesSides) { ExecSlopes(9); ExecSlopes(10); ExecSlopes(11); ExecSlopes(12); } void ExecSlopes(int content) { @@ -343,39 +318,37 @@ void ExecSlopes(int content) var supportDirectionA = -probeDirectionA; var supportDirectionB = -probeDirectionB; - foreach (var g in bmp.Indices()) + // Multithreaded Slope Evaluation strictly over filled blocks + System.Threading.Tasks.Parallel.ForEach(activeBlocks, new System.Threading.Tasks.ParallelOptions { CancellationToken = ct }, g => { - ct.ThrowIfCancellationRequested(); - if (blueprint[g] != 0) - continue; + if (blueprint[g] != 0) return; - if(blueprint[g + probeDirectionA] != BlueprintMesh.NoContent || blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) + if (blueprint[g + probeDirectionA] != BlueprintMesh.NoContent || blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) { - continue; + return; } if (settings.SlopesMustBeSupported) { - if(blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) + if (blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) { - continue; + return; } } bmp[g] = content; - } + }); if (totalSlopePasses > 0) { - executedPasses++; - progress?.Report((0.70 + (0.30 * ((double)executedPasses / totalSlopePasses)), "Slope Evaluation...")); + int currentPass = System.Threading.Interlocked.Increment(ref executedPasses); + progress?.Report((0.70 + (0.30 * ((double)currentPass / totalSlopePasses)), "Slope Evaluation...")); } } progress?.Report((1.0, "Complete!")); return blueprint; } -} public class GridMesher { @@ -674,129 +647,130 @@ static Vector3i ToInt(Vector3d vec) } } -public static class VoxelizationKernel -{ - // GPU-safe math implementations - private static int Min(int a, int b) => a < b ? a : b; - private static int Max(int a, int b) => a > b ? a : b; - private static float Min(float a, float b) => a < b ? a : b; - private static float Max(float a, float b) => a > b ? a : b; - private static float Abs(float v) => v < 0f ? -v : v; - private static float Min3(float a, float b, float c) => Min(a, Min(b, c)); - private static float Max3(float a, float b, float c) => Max(a, Max(b, c)); - private static int Floor(float val) => val < 0f ? (int)val - 1 : (int)val; - private static int Ceiling(float val) => val > (int)val ? (int)val + 1 : (int)val; - - public static void Voxelize( - Index1D index, - ArrayView triangles, - ArrayView voxelGrid, - Float3 gridOrigin, - float cellSize, - int gridX, - int gridY, - int gridZ) + public static class VoxelizationKernel { - var tri = triangles[index]; + // GPU-safe math implementations + private static int Min(int a, int b) => a < b ? a : b; + private static int Max(int a, int b) => a > b ? a : b; + private static float Min(float a, float b) => a < b ? a : b; + private static float Max(float a, float b) => a > b ? a : b; + private static float Abs(float v) => v < 0f ? -v : v; + private static float Min3(float a, float b, float c) => Min(a, Min(b, c)); + private static float Max3(float a, float b, float c) => Max(a, Max(b, c)); + private static int Floor(float val) => val < 0f ? (int)val - 1 : (int)val; + private static int Ceiling(float val) => val > (int)val ? (int)val + 1 : (int)val; + + public static void Voxelize( + Index1D index, + ArrayView triangles, + ArrayView voxelGrid, + Float3 gridOrigin, + float cellSize, + int gridX, + int gridY, + int gridZ) + { + var tri = triangles[index]; - int minX = Max(0, Floor((tri.MinBounds.X - gridOrigin.X) / cellSize)); - int minY = Max(0, Floor((tri.MinBounds.Y - gridOrigin.Y) / cellSize)); - int minZ = Max(0, Floor((tri.MinBounds.Z - gridOrigin.Z) / cellSize)); + int minX = Max(0, Floor((tri.MinBounds.X - gridOrigin.X) / cellSize)); + int minY = Max(0, Floor((tri.MinBounds.Y - gridOrigin.Y) / cellSize)); + int minZ = Max(0, Floor((tri.MinBounds.Z - gridOrigin.Z) / cellSize)); - int maxX = Min(gridX - 1, Ceiling((tri.MaxBounds.X - gridOrigin.X) / cellSize)); - int maxY = Min(gridY - 1, Ceiling((tri.MaxBounds.Y - gridOrigin.Y) / cellSize)); - int maxZ = Min(gridZ - 1, Ceiling((tri.MaxBounds.Z - gridOrigin.Z) / cellSize)); + int maxX = Min(gridX - 1, Ceiling((tri.MaxBounds.X - gridOrigin.X) / cellSize)); + int maxY = Min(gridY - 1, Ceiling((tri.MaxBounds.Y - gridOrigin.Y) / cellSize)); + int maxZ = Min(gridZ - 1, Ceiling((tri.MaxBounds.Z - gridOrigin.Z) / cellSize)); - // OPTIMIZATION 1: Precompute triangle edges and normal outside the loop - float e0X = tri.V1.X - tri.V0.X; float e0Y = tri.V1.Y - tri.V0.Y; float e0Z = tri.V1.Z - tri.V0.Z; - float e1X = tri.V2.X - tri.V1.X; float e1Y = tri.V2.Y - tri.V1.Y; float e1Z = tri.V2.Z - tri.V1.Z; - float e2X = tri.V0.X - tri.V2.X; float e2Y = tri.V0.Y - tri.V2.Y; float e2Z = tri.V0.Z - tri.V2.Z; + // OPTIMIZATION 1: Precompute triangle edges and normal outside the loop + float e0X = tri.V1.X - tri.V0.X; float e0Y = tri.V1.Y - tri.V0.Y; float e0Z = tri.V1.Z - tri.V0.Z; + float e1X = tri.V2.X - tri.V1.X; float e1Y = tri.V2.Y - tri.V1.Y; float e1Z = tri.V2.Z - tri.V1.Z; + float e2X = tri.V0.X - tri.V2.X; float e2Y = tri.V0.Y - tri.V2.Y; float e2Z = tri.V0.Z - tri.V2.Z; - float normalX = e0Y * e1Z - e0Z * e1Y; - float normalY = e0Z * e1X - e0X * e1Z; - float normalZ = e0X * e1Y - e0Y * e1X; + float normalX = e0Y * e1Z - e0Z * e1Y; + float normalY = e0Z * e1X - e0X * e1Z; + float normalZ = e0X * e1Y - e0Y * e1X; - for (int z = minZ; z <= maxZ; z++) - { - for (int y = minY; y <= maxY; y++) + for (int z = minZ; z <= maxZ; z++) { - for (int x = minX; x <= maxX; x++) + for (int y = minY; y <= maxY; y++) { - Float3 cellCenter = new Float3( - gridOrigin.X + (x + 0.5f) * cellSize, - gridOrigin.Y + (y + 0.5f) * cellSize, - gridOrigin.Z + (z + 0.5f) * cellSize - ); - - // Pass precomputed values into the intersection test - if (CheckTriangleBoxIntersection(tri, cellCenter, cellSize, - e0X, e0Y, e0Z, e1X, e1Y, e1Z, e2X, e2Y, e2Z, normalX, normalY, normalZ)) + for (int x = minX; x <= maxX; x++) { - int flatIndex = x + (y * gridX) + (z * gridX * gridY); - - // OPTIMIZATION 2: Cache-friendly early exit prevents memory bus locking - if (voxelGrid[flatIndex] != 0) + Float3 cellCenter = new Float3( + gridOrigin.X + (x + 0.5f) * cellSize, + gridOrigin.Y + (y + 0.5f) * cellSize, + gridOrigin.Z + (z + 0.5f) * cellSize + ); + + // Pass precomputed values into the intersection test + if (CheckTriangleBoxIntersection(tri, cellCenter, cellSize, + e0X, e0Y, e0Z, e1X, e1Y, e1Z, e2X, e2Y, e2Z, normalX, normalY, normalZ)) { - Atomic.Exchange(ref voxelGrid[flatIndex], 0); + int flatIndex = x + (y * gridX) + (z * gridX * gridY); + + // OPTIMIZATION 2: Cache-friendly early exit prevents memory bus locking + if (voxelGrid[flatIndex] != 0) + { + Atomic.Exchange(ref voxelGrid[flatIndex], 0); + } } } } } } - } - private static bool CheckTriangleBoxIntersection( - GpuTriangle tri, Float3 boxCenter, float cellSize, - float e0X, float e0Y, float e0Z, - float e1X, float e1Y, float e1Z, - float e2X, float e2Y, float e2Z, - float normalX, float normalY, float normalZ) - { - float boxHalf = cellSize * 0.5f; + private static bool CheckTriangleBoxIntersection( + GpuTriangle tri, Float3 boxCenter, float cellSize, + float e0X, float e0Y, float e0Z, + float e1X, float e1Y, float e1Z, + float e2X, float e2Y, float e2Z, + float normalX, float normalY, float normalZ) + { + float boxHalf = cellSize * 0.5f; - // Shift triangle to local AABB coordinate space - float v0X = tri.V0.X - boxCenter.X; float v0Y = tri.V0.Y - boxCenter.Y; float v0Z = tri.V0.Z - boxCenter.Z; - float v1X = tri.V1.X - boxCenter.X; float v1Y = tri.V1.Y - boxCenter.Y; float v1Z = tri.V1.Z - boxCenter.Z; - float v2X = tri.V2.X - boxCenter.X; float v2Y = tri.V2.Y - boxCenter.Y; float v2Z = tri.V2.Z - boxCenter.Z; + // Shift triangle to local AABB coordinate space + float v0X = tri.V0.X - boxCenter.X; float v0Y = tri.V0.Y - boxCenter.Y; float v0Z = tri.V0.Z - boxCenter.Z; + float v1X = tri.V1.X - boxCenter.X; float v1Y = tri.V1.Y - boxCenter.Y; float v1Z = tri.V1.Z - boxCenter.Z; + float v2X = tri.V2.X - boxCenter.X; float v2Y = tri.V2.Y - boxCenter.Y; float v2Z = tri.V2.Z - boxCenter.Z; - // SAT Test 1: Box AABB bounds - if (Min3(v0X, v1X, v2X) > boxHalf || Max3(v0X, v1X, v2X) < -boxHalf) return false; - if (Min3(v0Y, v1Y, v2Y) > boxHalf || Max3(v0Y, v1Y, v2Y) < -boxHalf) return false; - if (Min3(v0Z, v1Z, v2Z) > boxHalf || Max3(v0Z, v1Z, v2Z) < -boxHalf) return false; + // SAT Test 1: Box AABB bounds + if (Min3(v0X, v1X, v2X) > boxHalf || Max3(v0X, v1X, v2X) < -boxHalf) return false; + if (Min3(v0Y, v1Y, v2Y) > boxHalf || Max3(v0Y, v1Y, v2Y) < -boxHalf) return false; + if (Min3(v0Z, v1Z, v2Z) > boxHalf || Max3(v0Z, v1Z, v2Z) < -boxHalf) return false; - // SAT Test 2: Triangle Plane vs Box Overlap - float d = -(normalX * v0X + normalY * v0Y + normalZ * v0Z); + // SAT Test 2: Triangle Plane vs Box Overlap + float d = -(normalX * v0X + normalY * v0Y + normalZ * v0Z); - float vminX = normalX > 0f ? -boxHalf : boxHalf; float vmaxX = normalX > 0f ? boxHalf : -boxHalf; - float vminY = normalY > 0f ? -boxHalf : boxHalf; float vmaxY = normalY > 0f ? boxHalf : -boxHalf; - float vminZ = normalZ > 0f ? -boxHalf : boxHalf; float vmaxZ = normalZ > 0f ? boxHalf : -boxHalf; + float vminX = normalX > 0f ? -boxHalf : boxHalf; float vmaxX = normalX > 0f ? boxHalf : -boxHalf; + float vminY = normalY > 0f ? -boxHalf : boxHalf; float vmaxY = normalY > 0f ? boxHalf : -boxHalf; + float vminZ = normalZ > 0f ? -boxHalf : boxHalf; float vmaxZ = normalZ > 0f ? boxHalf : -boxHalf; - if ((normalX * vminX + normalY * vminY + normalZ * vminZ) + d > 0f) return false; - if ((normalX * vmaxX + normalY * vmaxY + normalZ * vmaxZ) + d < 0f) return false; + if ((normalX * vminX + normalY * vminY + normalZ * vminZ) + d > 0f) return false; + if ((normalX * vmaxX + normalY * vmaxY + normalZ * vmaxZ) + d < 0f) return false; - // SAT Test 3: Edge Cross Products - if (!AxisTest(e0Z, -e0Y, v0Y, v0Z, v2Y, v2Z, boxHalf)) return false; - if (!AxisTest(e1Z, -e1Y, v1Y, v1Z, v0Y, v0Z, boxHalf)) return false; - if (!AxisTest(e2Z, -e2Y, v2Y, v2Z, v1Y, v1Z, boxHalf)) return false; + // SAT Test 3: Edge Cross Products + if (!AxisTest(e0Z, -e0Y, v0Y, v0Z, v2Y, v2Z, boxHalf)) return false; + if (!AxisTest(e1Z, -e1Y, v1Y, v1Z, v0Y, v0Z, boxHalf)) return false; + if (!AxisTest(e2Z, -e2Y, v2Y, v2Z, v1Y, v1Z, boxHalf)) return false; - if (!AxisTest(-e0Z, e0X, v0X, v0Z, v2X, v2Z, boxHalf)) return false; - if (!AxisTest(-e1Z, e1X, v1X, v1Z, v0X, v0Z, boxHalf)) return false; - if (!AxisTest(-e2Z, e2X, v2X, v2Z, v1X, v1Z, boxHalf)) return false; + if (!AxisTest(-e0Z, e0X, v0X, v0Z, v2X, v2Z, boxHalf)) return false; + if (!AxisTest(-e1Z, e1X, v1X, v1Z, v0X, v0Z, boxHalf)) return false; + if (!AxisTest(-e2Z, e2X, v2X, v2Z, v1X, v1Z, boxHalf)) return false; - if (!AxisTest(e0Y, -e0X, v0X, v0Y, v2X, v2Y, boxHalf)) return false; - if (!AxisTest(e1Y, -e1X, v1X, v1Y, v0X, v0Y, boxHalf)) return false; - if (!AxisTest(e2Y, -e2X, v2X, v2Y, v1X, v1Y, boxHalf)) return false; + if (!AxisTest(e0Y, -e0X, v0X, v0Y, v2X, v2Y, boxHalf)) return false; + if (!AxisTest(e1Y, -e1X, v1X, v1Y, v0X, v0Y, boxHalf)) return false; + if (!AxisTest(e2Y, -e2X, v2X, v2Y, v1X, v1Y, boxHalf)) return false; - return true; - } + return true; + } - private static bool AxisTest(float a, float b, float fa, float fb, float va, float vb, float boxHalf) - { - float p0 = a * fa + b * fb; - float p2 = a * va + b * vb; - float min = Min(p0, p2); - float max = Max(p0, p2); - float rad = (Abs(a) + Abs(b)) * boxHalf; - return !(min > rad || max < -rad); + private static bool AxisTest(float a, float b, float fa, float fb, float va, float vb, float boxHalf) + { + float p0 = a * fa + b * fb; + float p2 = a * va + b * vb; + float min = Min(p0, p2); + float max = Max(p0, p2); + float rad = (Abs(a) + Abs(b)) * boxHalf; + return !(min > rad || max < -rad); + } } } \ No newline at end of file diff --git a/Controls/BlueprintGenerator.xaml.cs b/Controls/BlueprintGenerator.xaml.cs index a764537..ab2c544 100644 --- a/Controls/BlueprintGenerator.xaml.cs +++ b/Controls/BlueprintGenerator.xaml.cs @@ -4,10 +4,7 @@ using SpaceEditor.Algorithms; using SpaceEditor.Data; using SpaceEditor.Rocks; -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; @@ -30,7 +27,7 @@ public record ModelSettings [ButtonProperty(nameof(RecenterImp))] public bool Recenter { get; set; } - + public float ModelSize { get; set; } [ButtonProperty(nameof(ScareToTargetSize))] @@ -57,13 +54,13 @@ private void ScareToTargetSize() { var bb = model.CachedBounds; var dimensions = bb.Extents * 2; - + var size = dimensions.MaxAbs; var targetSize = Math.Max(this.ModelSize, 1); MeshTransforms.Scale ( - model, + model, new(targetSize / size), bb.Center ); @@ -123,7 +120,7 @@ public partial class BlueprintGenerator : UserControl public DMesh3? Model; public AsyncLazy? ModelBVH; public CancellationTokenSource? ModelLifetime; - + public CancellationTokenSource? GeneratorLifetime; private void UserControl_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) @@ -137,12 +134,12 @@ private void UserControl_PreviewKeyDown(object sender, System.Windows.Input.KeyE private ModelSettings ModelSettingsVM { - get => (ModelSettings) this.ModelSettings.ReflectedInstance; + get => (ModelSettings)this.ModelSettings.ReflectedInstance; set { // Make sure to reload the new values this.ModelSettings.ReflectedInstance = null!; - + this.ModelSettings.ReflectedInstance = value; } } @@ -172,7 +169,7 @@ private void SelectModel(object sender, RoutedEventArgs e) model = LoadModel(selectFile.FileName); this.BlueprintName.Text = Path.GetFileNameWithoutExtension(selectFile.FileName); - + } catch @@ -202,7 +199,7 @@ public void SetNewModel(DMesh3? model) this.ModelBVH.Poke(); var bb = this.Model.CachedBounds; - this.ModelSettingsVM = this.ModelSettingsVM with { ModelSize = (float) bb.Extents.MaxAbs * 2 }; + this.ModelSettingsVM = this.ModelSettingsVM with { ModelSize = (float)bb.Extents.MaxAbs * 2 }; var modelInfo = new StringBuilder(); modelInfo.AppendLine($"Model: {this.BlueprintName.Text}"); @@ -239,6 +236,7 @@ public void SetNewModel(DMesh3? model) private async void GenerateBlueprint(object sender, RoutedEventArgs e) { + string mode; // Prevent accidental double-clicks from spawning multiple tasks if (this.IsGenerating) return; this.IsGenerating = true; @@ -281,6 +279,9 @@ private async void GenerateBlueprint(object sender, RoutedEventArgs e) BlueprintMesh blueprint; + // Start the high-precision stopwatch right before dispatching + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + try { // Offload execution to a background thread, routing to CPU or GPU based on the flag @@ -308,7 +309,8 @@ private async void GenerateBlueprint(object sender, RoutedEventArgs e) this.ProgressText.Visibility = Visibility.Collapsed; this.ProgressText.Text = string.Empty; - this.IsGenerating = false; // Unlock the UI + // Do NOT unlock IsGenerating here or stop the stopwatch yet. + // The UI thread still needs to build and render the mesh! } // The code below automatically resumes on the UI thread @@ -327,7 +329,13 @@ private async void GenerateBlueprint(object sender, RoutedEventArgs e) Vector3i dimensions = GetBlueprintDiemensions(usedIndicies); - string info = $"Blocks: X: {dimensions.x} Y: {dimensions.y} Z: {dimensions.z} Total: {usedIndicies.Count()}"; + // The meshing and viewport rendering is finished. Now we stop the stopwatch. + stopwatch.Stop(); + + mode = isCpuFallback ? "CPU" : "GPU"; + System.Diagnostics.Debug.WriteLine($"\n[PERFORMANCE] Blueprint generated in {stopwatch.Elapsed.TotalSeconds:F3} seconds using {mode}.\n"); + + string info = $"Blocks: X: {dimensions.x} Y: {dimensions.y} Z: {dimensions.z} Total: {usedIndicies.Count()}\nTime: {stopwatch.Elapsed.TotalSeconds:F2}s (generated using {mode})"; this.BlueprintDetails.Text = info; lifetime.Register(() => @@ -347,7 +355,7 @@ private async void GenerateBlueprint(object sender, RoutedEventArgs e) } finally { - this.IsGenerating = false; // Failsafe unlock + this.IsGenerating = false; // Failsafe unlock AFTER everything is done } } @@ -355,14 +363,14 @@ public static Vector3i GetBlueprintDiemensions(IEnumerable indicies) { AxisAlignedBox3i dimensions = AxisAlignedBox3i.Empty; - foreach (Vector3i v3i in indicies) + foreach (Vector3i v3i in indicies) { dimensions.Contain(v3i); } return dimensions.Diagonal + 1; } - + private void ExportBlueprint(object sender, RoutedEventArgs e) { var blueprint = (BlueprintMesh)this.ExportBlueprintPanel.Tag; @@ -391,10 +399,10 @@ private void ExportBlueprintAsModel(object sender, RoutedEventArgs e) saveLocation.AddExtension = true; saveLocation.DefaultExt = "obj"; saveLocation.FileName = this.BlueprintName.Text; - + if (saveLocation.ShowDialog() != true) return; - + var mesh = GridMesher.Mesh(blueprint); Util.WriteDebugMesh(mesh, saveLocation.FileName); } @@ -409,7 +417,7 @@ private DMesh3 LoadModel(string path) foreach (var m in scene.Meshes) { var vertexIndices = new List(); - + var vertices = m.Vertices; var normals = m.HasNormals ? m.Normals : null; var uvs = m.HasTextureCoords(0) ? m.TextureCoordinateChannels[0] : null; @@ -484,15 +492,15 @@ private IDisposable CreateRenderModel(DMesh3 model, int controlsRow) { var controls = this.ViewportControls.Children.OfType().Where(x => Grid.GetRow(x) == controlsRow).ToArray(); - var draw = (CheckBox) controls[1]; - var xray = (CheckBox) controls[2]; - var color = (ColourSlider) controls[3]; + var draw = (CheckBox)controls[1]; + var xray = (CheckBox)controls[2]; + var color = (ColourSlider)controls[3]; var renderData = new ModelViewport.ModelData { Mesh = model, }; - + RoutedEventHandler onCheckedChanged = (_, _) => { renderData.Color = color.SelectedColour; diff --git a/Data/GameProxy.cs b/Data/GameProxy.cs index d8d4a6e..4eac966 100644 --- a/Data/GameProxy.cs +++ b/Data/GameProxy.cs @@ -98,6 +98,13 @@ public class GameProxy public AsyncLazy InputIds { get; } public AsyncLazy InputActions { get; } + // Pre-cached reflection types to eliminate massive loop bottlenecks + private readonly Type _customSerializationContextType; + private readonly Type _serializationContextType; + private readonly Type _serializationHelperType; + private readonly object _jsonFormatEnumValue; + private readonly MethodInfo _serializeAbstractMethod; + public GameProxy(string baseGamePath) { this.BaseGamePath = baseGamePath; @@ -112,6 +119,13 @@ public GameProxy(string baseGamePath) var md = st.AsDynamicType().GetInstance(mdt); md.PushContext(new[] { se2 }); + // CACHE TYPES ONCE AT STARTUP + _customSerializationContextType = FindType("CustomSerializationContext"); + _serializationContextType = FindType("SerializationContext"); + _serializationHelperType = FindType("SerializationHelper"); + _jsonFormatEnumValue = Enum.Parse(FindType("SerializerFormat"), "Json"); + _serializeAbstractMethod = _serializationHelperType.GetMethod("SerializeAbstract")!.MakeGenericMethod(typeof(object)); + this.InputIds = new(LoadInputIds); this.InputActions = new(LoadInputActions); } @@ -124,31 +138,31 @@ public Type FindType(string typeName) public dynamic DeserializeFile(string filePath, params object[] services) { - using var fs = new FileStream(filePath, FileMode.Open); + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); return DeserializeObject(fs, services); } public dynamic DeserializeObject(Stream content, params object[] services) { - var typedServices = Array.CreateInstance(FindType("CustomSerializationContext"), services.Length); + // Use pre-cached types instead of FindType() and Enum.Parse() + var typedServices = Array.CreateInstance(_customSerializationContextType, services.Length); Array.Copy(services, typedServices, services.Length); - var format = Enum.Parse(FindType("SerializerFormat"), "Json"); - - using var sc = (IDisposable)Activator.CreateInstance(FindType("SerializationContext"), content, "NoName.txt", typedServices)!; - return FindType("SerializationHelper").AsDynamicType().DeserializeAbstract(sc, format); + using var sc = (IDisposable)Activator.CreateInstance(_serializationContextType, content, "NoName.txt", typedServices)!; + return _serializationHelperType.AsDynamicType().DeserializeAbstract(sc, _jsonFormatEnumValue); } public string SerializeObject(object instance, params object[] services) { - var typedServices = Array.CreateInstance(FindType("CustomSerializationContext"), services.Length); + // Use pre-cached types + var typedServices = Array.CreateInstance(_customSerializationContextType, services.Length); Array.Copy(services, typedServices, services.Length); - var format = Enum.Parse(FindType("SerializerFormat"), "Json"); - using var data = new MemoryStream(); - using var sc = (IDisposable)Activator.CreateInstance(FindType("SerializationContext"), data, "NoName.txt", typedServices)!; - FindType("SerializationHelper").GetMethod("SerializeAbstract")!.MakeGenericMethod(typeof(object)).Invoke(null, [sc, instance, format]); + using var sc = (IDisposable)Activator.CreateInstance(_serializationContextType, data, "NoName.txt", typedServices)!; + + // Use pre-cached MethodInfo + _serializeAbstractMethod.Invoke(null, [sc, instance, _jsonFormatEnumValue]); return Encoding.UTF8.GetString(data.GetBuffer().AsSpan()[..(int)data.Length]); } @@ -156,18 +170,21 @@ public string SerializeObject(object instance, params object[] services) private InputActions LoadInputActions() { var actions = new InputActions(); - var inputActionDefinitionType = FindType("InputActionDefinition"); - var actionsDir = GameFacts.GetActionsPath(this.BaseGamePath); + + // Dedicated lock object prevents global thread locking issues + object syncObj = new object(); + Parallel.ForEach(Directory.EnumerateFiles(actionsDir), file => { try { var def = DeserializeFile(file); - Guid id = def.Guid; - lock (actionsDir) + + // Safely lock using the dedicated object + lock (syncObj) { actions.Actions.Add(id, new InputActions.InputActionInfo { @@ -182,7 +199,6 @@ private InputActions LoadInputActions() { } }); - var proxy = new ProxyGenerator(); var mapGuidToDefinitionInstance = proxy.CreateClassProxy ( diff --git a/MainWindow.xaml b/MainWindow.xaml index bfdb96e..a8d99b2 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -10,7 +10,7 @@ Title="Space Editor" Height="550" Width="1000" - d:DataContext="{local:MainWindow}" + d:DataContext="{d:DesignInstance Type=local:MainWindow, IsDesignTimeCreatable=True}" > Date: Mon, 27 Jul 2026 00:30:49 +0200 Subject: [PATCH 6/8] Refactor GPU voxelization and safe file parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GridShaper: Unified the blueprint generation methods, since a substantial amount of the code was identical. GpuSetup now exposes SyncLock, private-set context/accelerator/kernels and a RebuildContext() to safely reinitialize ILGPU (including InitGridKernel). This should prevent the exceptions when the user generates a blueprint, picks up a different model (without saving the previous one) and tries to generate that one. GameProxy: made action-file loading resilient — filter for .sbc/.json, fast-skip files without a Guid, null-checks after deserialization, safer reflection when accessing Id, and broader exception handling to avoid runtime binder/serialization crashes. This should make the game file loading faster, especially when debugging. --- Algorithms/GridShaper.cs | 363 ++++++++++++++++++--------------------- Data/GameProxy.cs | 44 ++++- 2 files changed, 204 insertions(+), 203 deletions(-) diff --git a/Algorithms/GridShaper.cs b/Algorithms/GridShaper.cs index 09c2a54..10c98ed 100644 --- a/Algorithms/GridShaper.cs +++ b/Algorithms/GridShaper.cs @@ -36,22 +36,41 @@ public GridShaper(DMesh3 mesh, DMeshAABBTree3 tree) this.Tree = tree; } - // Class to cache the GPU context and kernel globally + // Class to cache the GPU context and kernels globally public static class GpuSetup { - public static Context Context { get; } - public static Accelerator Accelerator { get; } - public static Action, ArrayView, Float3, float, int, int, int> VoxelizeKernel { get; } + public static Context Context { get; private set; } + public static Accelerator Accelerator { get; private set; } + public static Action, ArrayView, Float3, float, int, int, int> VoxelizeKernel { get; private set; } + public static Action, int> InitGridKernel { get; private set; } + + public static readonly object SyncLock = new object(); static GpuSetup() { - Context = Context.CreateDefault(); - Accelerator = Context.GetPreferredDevice(preferCPU: false).CreateAccelerator(Context); - System.Diagnostics.Debug.WriteLine($"\n[ILGPU INITIALIZATION] Compiled and Cached on: {Accelerator.Name} (Type: {Accelerator.AcceleratorType})\n"); + RebuildContext(); + } + + public static void RebuildContext() + { + lock (SyncLock) + { + // Force ILGPU to release all cached VRAM pools back to the OS + try { Accelerator?.Dispose(); } catch { } + try { Context?.Dispose(); } catch { } + + Context = Context.CreateDefault(); + Accelerator = Context.GetPreferredDevice(preferCPU: false).CreateAccelerator(Context); + System.Diagnostics.Debug.WriteLine($"\n[ILGPU INITIALIZATION] Compiled and Cached on: {Accelerator.Name} (Type: {Accelerator.AcceleratorType})\n"); + + VoxelizeKernel = Accelerator.LoadAutoGroupedStreamKernel< + Index1D, ArrayView, ArrayView, Float3, float, int, int, int>( + VoxelizationKernel.Voxelize); - VoxelizeKernel = Accelerator.LoadAutoGroupedStreamKernel< - Index1D, ArrayView, ArrayView, Float3, float, int, int, int>( - VoxelizationKernel.Voxelize); + InitGridKernel = Accelerator.LoadAutoGroupedStreamKernel< + Index1D, ArrayView, int>( + VoxelizationKernel.InitializeGrid); + } } } @@ -81,227 +100,190 @@ public class GeneratorSettings ]; } + // --- PUBLIC FACADES --- + // These methods are the entry points for generating a blueprint mesh, either using GPU acceleration or CPU processing. public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, IProgress<(double, string)> progress = null) + => CoreGenerate(settings, useGpu: true, ct, progress); + + public BlueprintMesh GenerateCpu(GeneratorSettings settings, CancellationToken ct, IProgress<(double, string)> progress = null) + => CoreGenerate(settings, useGpu: false, ct, progress); + + // --- UNIFIED GENERATOR --- + private BlueprintMesh CoreGenerate(GeneratorSettings settings, bool useGpu, CancellationToken ct, IProgress<(double, string)> progress) { + // ========================================== + // PHASE 1: SHARED SETUP & GRID ALLOCATION + // ========================================== var blockSize = settings.BlockSize switch { BlockSizes.TwoPointFive => ShapeDB.LargeBlockSize, BlockSizes.HalfMeter => ShapeDB.MidBlockSize }; - var minimalBounds = this.Tree.Bounds; - minimalBounds.Min -= blockSize; - minimalBounds.Max += blockSize; + var bounds = this.Tree.Bounds; + bounds.Expand(blockSize); - var boundingBox = new g4.AxisAlignedBox3d(new g4.Vector3d(0), blockSize / 2); - while (boundingBox.Contains(minimalBounds) == false) + int gridX = (int)Math.Ceiling(bounds.Width / blockSize); + int gridY = (int)Math.Ceiling(bounds.Height / blockSize); + int gridZ = (int)Math.Ceiling(bounds.Depth / blockSize); + + long totalGridVolume = (long)gridX * gridY * gridZ; + + if (totalGridVolume >= 67_108_864) { - boundingBox.Scale(2, 2, 2); + throw new Exception($"Model is too large for {settings.BlockSize} resolution!\n" + + $"Requires {totalGridVolume:N0} blocks ({gridX}x{gridY}x{gridZ}).\n" + + $"Please scale the model down or select a larger block size."); + } + if (this.Mesh.TriangleCount == 0) + { + throw new Exception("The selected model contains no 3D geometry."); } - var cellCount = (int)Math.Ceiling(boundingBox.MaxDim / blockSize); - var indexer = new ShiftGridIndexer3(boundingBox.Min, blockSize); + var indexer = new ShiftGridIndexer3(bounds.Min, blockSize); + var bmp = new DenseGrid3i(gridX, gridY, gridZ, BlueprintMesh.NoContent); + int triangleCount = this.Mesh.TriangleCount; - // Access the cached GPU environment instead of creating a new one - var accelerator = GpuSetup.Accelerator; - var voxelizeKernel = GpuSetup.VoxelizeKernel; - System.Diagnostics.Debug.WriteLine($"\n[ILGPU VERIFICATION] Executing on: {accelerator.Name} (Type: {accelerator.AcceleratorType})\n"); + var blueprint = new BlueprintMesh + { + Blocks = bmp, + Coords = indexer, + Shapes = settings.BlockSize switch + { + BlockSizes.TwoPointFive => ShapeDB.LargeShapes, + BlockSizes.HalfMeter => ShapeDB.MidShapes + } + }; - // Prepare Triangle Data - int triangleCount = this.Mesh.TriangleCount; - var flatTriangles = new GpuTriangle[triangleCount]; - int tIndex = 0; + IEnumerable activeBlocks; - // PHASE 1: Triangles (0% - 10%) - foreach (var triangle in this.Mesh.EnumerateTriangles()) + // ========================================== + // PHASE 2: VOXELIZATION (BRANCHING) + // ========================================== + if (useGpu) { - ct.ThrowIfCancellationRequested(); - if (tIndex % 5000 == 0) progress?.Report((0.1 * ((double)tIndex / triangleCount), "Mesh Flattening...")); + Accelerator accelerator; + Action, ArrayView, Float3, float, int, int, int> voxelizeKernel; + Action, int> initGridKernel; - var box = triangle.ToBox(); - flatTriangles[tIndex++] = new GpuTriangle + lock (GpuSetup.SyncLock) { - V0 = new Float3((float)triangle.V0.x, (float)triangle.V0.y, (float)triangle.V0.z), - V1 = new Float3((float)triangle.V1.x, (float)triangle.V1.y, (float)triangle.V1.z), - V2 = new Float3((float)triangle.V2.x, (float)triangle.V2.y, (float)triangle.V2.z), - MinBounds = new Float3((float)box.Min.x, (float)box.Min.y, (float)box.Min.z), - MaxBounds = new Float3((float)box.Max.x, (float)box.Max.y, (float)box.Max.z) - }; - } - - using var deviceTriangles = accelerator.Allocate1D(flatTriangles); - int totalCells = cellCount * cellCount * cellCount; - int[] initialGrid = new int[totalCells]; - Array.Fill(initialGrid, BlueprintMesh.NoContent); - using var deviceGrid = accelerator.Allocate1D(initialGrid); - - Float3 origin = new Float3((float)boundingBox.Min.x, (float)boundingBox.Min.y, (float)boundingBox.Min.z); - - voxelizeKernel( - deviceTriangles.IntExtent, - deviceTriangles.View, - deviceGrid.View, - origin, - blockSize, - cellCount, - cellCount, - cellCount - ); + accelerator = GpuSetup.Accelerator; + voxelizeKernel = GpuSetup.VoxelizeKernel; + initGridKernel = GpuSetup.InitGridKernel; + } - // PHASE 2: Voxelization (Jump to 40% after sync) - accelerator.Synchronize(); - progress?.Report((0.40, "Voxelization...")); - var flatResults = deviceGrid.GetAsArray1D(); + System.Diagnostics.Debug.WriteLine($"\n[ILGPU VERIFICATION] Executing on: {accelerator.Name} (Type: {accelerator.AcceleratorType})\n"); - var bmp = new DenseGrid3i(cellCount, cellCount, cellCount, BlueprintMesh.NoContent); + var flatTriangles = new GpuTriangle[triangleCount]; + int tIndex = 0; - // PHASE 3: Grid Reconstruction (40% - 70%) - int processedZ = 0; - var activeBlocks = new System.Collections.Concurrent.ConcurrentBag(); - Parallel.For(0, cellCount, new ParallelOptions { CancellationToken = ct }, z => - { - for (int y = 0; y < cellCount; y++) + foreach (var triangle in this.Mesh.EnumerateTriangles()) { - for (int x = 0; x < cellCount; x++) + ct.ThrowIfCancellationRequested(); + if (tIndex % 5000 == 0) progress?.Report((0.1 * ((double)tIndex / triangleCount), "Mesh Flattening...")); + + var box = triangle.ToBox(); + flatTriangles[tIndex++] = new GpuTriangle { - int flatIdx = x + (y * cellCount) + (z * cellCount * cellCount); - if (flatResults[flatIdx] == 0) - { - var cell = new g4.Vector3i(x, y, z); - bmp[cell] = 0; - activeBlocks.Add(cell); - } - } + V0 = new Float3((float)triangle.V0.x, (float)triangle.V0.y, (float)triangle.V0.z), + V1 = new Float3((float)triangle.V1.x, (float)triangle.V1.y, (float)triangle.V1.z), + V2 = new Float3((float)triangle.V2.x, (float)triangle.V2.y, (float)triangle.V2.z), + MinBounds = new Float3((float)box.Min.x, (float)box.Min.y, (float)box.Min.z), + MaxBounds = new Float3((float)box.Max.x, (float)box.Max.y, (float)box.Max.z) + }; } - int currentZ = Interlocked.Increment(ref processedZ); - if (currentZ % 10 == 0) + int[] flatResults; + int totalCells = (int)totalGridVolume; + + try { - progress?.Report((0.40 + (0.30 * ((double)currentZ / cellCount)), "Grid Reconstruction...")); - } - }); + using var deviceTriangles = accelerator.Allocate1D(flatTriangles); + using var deviceGrid = accelerator.Allocate1D(totalCells); - var blueprint = new BlueprintMesh(); - blueprint.Blocks = bmp; - blueprint.Coords = indexer; - blueprint.Shapes = settings.BlockSize switch - { - BlockSizes.TwoPointFive => ShapeDB.LargeShapes, - BlockSizes.HalfMeter => ShapeDB.MidShapes - }; + initGridKernel(totalCells, deviceGrid.View, BlueprintMesh.NoContent); - // PHASE 4: Slope Generation (70% - 100%) - progress?.Report((0.70, "Slope Evaluation...")); - int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); - int executedPasses = 0; + Float3 origin = new Float3((float)bounds.Min.x, (float)bounds.Min.y, (float)bounds.Min.z); - if (settings.SlopesUpper) { ExecSlopes(1); ExecSlopes(2); ExecSlopes(3); ExecSlopes(4); } - if (settings.SlopesLower) { ExecSlopes(5); ExecSlopes(6); ExecSlopes(7); ExecSlopes(8); } - if (settings.SlopesSides) { ExecSlopes(9); ExecSlopes(10); ExecSlopes(11); ExecSlopes(12); } + voxelizeKernel(deviceTriangles.IntExtent, deviceTriangles.View, deviceGrid.View, origin, blockSize, gridX, gridY, gridZ); - void ExecSlopes(int content) - { - var shapeInfo = blueprint.Shapes[content]; - var probeDirectionA = -Base6Directions.Vectors[shapeInfo.Forward]; - var probeDirectionB = Base6Directions.Vectors[shapeInfo.Up]; - var supportDirectionA = -probeDirectionA; - var supportDirectionB = -probeDirectionB; - - // Multithreaded Slope Evaluation strictly over filled blocks - System.Threading.Tasks.Parallel.ForEach(activeBlocks, new System.Threading.Tasks.ParallelOptions { CancellationToken = ct }, g => + accelerator.Synchronize(); + progress?.Report((0.40, "Voxelization...")); + flatResults = deviceGrid.GetAsArray1D(); + } + catch (Exception ex) { - if (blueprint[g] != 0) return; // 'return' breaks out of the lambda for this specific block + throw new Exception("The GPU driver failed during allocation. Out of VRAM or driver error.", ex); + } - if (blueprint[g + probeDirectionA] != BlueprintMesh.NoContent || blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) - { - return; - } + _ = Task.Run(() => GpuSetup.RebuildContext()); - if (settings.SlopesMustBeSupported) + int processedZ = 0; + var gpuActiveBlocks = new System.Collections.Concurrent.ConcurrentBag(); + + Parallel.For(0, gridZ, new ParallelOptions { CancellationToken = ct }, z => + { + for (int y = 0; y < gridY; y++) { - if (blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) + for (int x = 0; x < gridX; x++) { - return; + int flatIdx = x + (y * gridX) + (z * gridX * gridY); + if (flatResults[flatIdx] == 0) + { + var cell = new Vector3i(x, y, z); + bmp[cell] = 0; + gpuActiveBlocks.Add(cell); + } } } - bmp[g] = content; + int currentZ = Interlocked.Increment(ref processedZ); + if (currentZ % 10 == 0) + { + progress?.Report((0.40 + (0.30 * ((double)currentZ / gridZ)), "Grid Reconstruction...")); + } }); - if (totalSlopePasses > 0) - { - int currentPass = System.Threading.Interlocked.Increment(ref executedPasses); - progress?.Report((0.70 + (0.30 * ((double)currentPass / totalSlopePasses)), "Slope Evaluation...")); - } - } - - progress?.Report((1.0, "Complete, finalization!")); - return blueprint; - } - - public BlueprintMesh GenerateCpu(GeneratorSettings settings, CancellationToken ct, IProgress<(double, string)> progress = null) - { - var blockSize = settings.BlockSize switch - { - BlockSizes.TwoPointFive => ShapeDB.LargeBlockSize, - BlockSizes.HalfMeter => ShapeDB.MidBlockSize - }; - - var minimalBounds = this.Tree.Bounds; - minimalBounds.Min -= blockSize; - minimalBounds.Max += blockSize; - - var boundingBox = new AxisAlignedBox3d(new Vector3d(0), blockSize / 2); - while (boundingBox.Contains(minimalBounds) == false) - { - boundingBox.Scale(2, 2, 2); + activeBlocks = gpuActiveBlocks; } - - System.Diagnostics.Debug.WriteLine($"\nExecuting the model to blueprint conversion on the CPU\n"); - var cellCount = (int)Math.Ceiling(boundingBox.MaxDim / blockSize); - var indexer = new ShiftGridIndexer3(boundingBox.Min, blockSize); - - var bmp = new DenseGrid3i(cellCount, cellCount, cellCount, BlueprintMesh.NoContent); - - var blueprint = new BlueprintMesh(); - blueprint.Blocks = bmp; - blueprint.Coords = indexer; - blueprint.Shapes = settings.BlockSize switch + else { - BlockSizes.TwoPointFive => ShapeDB.LargeShapes, - BlockSizes.HalfMeter => ShapeDB.MidShapes - }; - - int triangleCount = this.Mesh.TriangleCount; - int tIndex = 0; + System.Diagnostics.Debug.WriteLine($"\nExecuting the model to blueprint conversion on the CPU\n"); + int tIndex = 0; - foreach (var triangle in this.Mesh.EnumerateTriangles()) - { - ct.ThrowIfCancellationRequested(); - if (tIndex % 1000 == 0) progress?.Report((0.7 * ((double)tIndex / triangleCount), "Triangle Evaluation...")); - tIndex++; - - var triBox = triangle.ToBox(); - foreach (var cell in Enumerators.BoxRange(triBox, indexer)) + foreach (var triangle in this.Mesh.EnumerateTriangles()) { - var cellBox = indexer.ToBox(cell); - if (cellBox.IntersectWithTriangle(triangle) != IntersectResult.NoIntersection) + ct.ThrowIfCancellationRequested(); + if (tIndex % 1000 == 0) progress?.Report((0.7 * ((double)tIndex / triangleCount), "Triangle Evaluation...")); + tIndex++; + + var triBox = triangle.ToBox(); + foreach (var cell in Enumerators.BoxRange(triBox, indexer)) { - bmp[cell] = 0; //Cube + var cellBox = indexer.ToBox(cell); + if (cellBox.IntersectWithTriangle(triangle) != IntersectResult.NoIntersection) + { + bmp[cell] = 0; + } } } - } - // OPTIMIZATION: Gather only the active blocks to eliminate millions of empty-space checks - var activeBlocks = new List(); - foreach (var g in bmp.Indices()) - { - if (bmp[g] == 0) + var cpuActiveBlocks = new List(); + foreach (var g in bmp.Indices()) { - activeBlocks.Add(g); + if (bmp[g] == 0) + { + cpuActiveBlocks.Add(g); + } } + + activeBlocks = cpuActiveBlocks; } + // ========================================== + // PHASE 3: SHARED SLOPE GENERATION + // ========================================== progress?.Report((0.70, "Slope Evaluation...")); int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); int executedPasses = 0; @@ -318,8 +300,7 @@ void ExecSlopes(int content) var supportDirectionA = -probeDirectionA; var supportDirectionB = -probeDirectionB; - // Multithreaded Slope Evaluation strictly over filled blocks - System.Threading.Tasks.Parallel.ForEach(activeBlocks, new System.Threading.Tasks.ParallelOptions { CancellationToken = ct }, g => + Parallel.ForEach(activeBlocks, new ParallelOptions { CancellationToken = ct }, g => { if (blueprint[g] != 0) return; @@ -341,12 +322,12 @@ void ExecSlopes(int content) if (totalSlopePasses > 0) { - int currentPass = System.Threading.Interlocked.Increment(ref executedPasses); + int currentPass = Interlocked.Increment(ref executedPasses); progress?.Report((0.70 + (0.30 * ((double)currentPass / totalSlopePasses)), "Slope Evaluation...")); } } - progress?.Report((1.0, "Complete!")); + progress?.Report((1.0, "Complete, finalization!")); return blueprint; } @@ -393,7 +374,6 @@ public static DMesh3 Mesh(BlueprintMesh blueprint) var cubesMesh = cubesSurfaceGenerator.Meshes[0]; MeshTransforms.Scale(cubesMesh, blueprint.Coords.CellSize); - // Voxel generator generates around UnitZeroCentered, while indexer rounds down to corner var correctionOffset = blueprint.Coords.CellSize / 2; MeshTransforms.Translate(cubesMesh, blueprint.Coords.Origin + correctionOffset); @@ -548,7 +528,6 @@ public void Write(BlueprintMesh blueprint, string name) public void Generate(BlueprintMesh blueprint, StringBuilder sb) { - //Prefab|PositionX|PositionY|PositionZ|ColorHUE|ColorSATURATION|ColorVALUE|OrientationFORWARD|OrientationUP|Integrity var blockGrid = blueprint.Blocks; foreach (var g in blockGrid.Indices()) { @@ -572,7 +551,6 @@ public void Generate(BlueprintMesh blueprint, StringBuilder sb) var gridPosition = ToInt(cube.Center / ShapeDB.SmallBlockSize); gridPosition += PositionOffset ( - //TODO: blueprint.Shapes == ShapeDB.LargeShapes ? new AxisAlignedBox3i(new Vector3i(-4, -4, -4), new Vector3i(5, 5, 5)) : new AxisAlignedBox3i(new Vector3i(0, 0, 0), new Vector3i(1, 1, 1)), @@ -580,7 +558,6 @@ public void Generate(BlueprintMesh blueprint, StringBuilder sb) upAxis ); - sb.Append(gridPosition.x); sb.Append('|'); sb.Append(gridPosition.y); @@ -660,6 +637,11 @@ public static class VoxelizationKernel private static int Floor(float val) => val < 0f ? (int)val - 1 : (int)val; private static int Ceiling(float val) => val > (int)val ? (int)val + 1 : (int)val; + public static void InitializeGrid(Index1D index, ArrayView grid, int value) + { + grid[index] = value; + } + public static void Voxelize( Index1D index, ArrayView triangles, @@ -680,7 +662,6 @@ public static void Voxelize( int maxY = Min(gridY - 1, Ceiling((tri.MaxBounds.Y - gridOrigin.Y) / cellSize)); int maxZ = Min(gridZ - 1, Ceiling((tri.MaxBounds.Z - gridOrigin.Z) / cellSize)); - // OPTIMIZATION 1: Precompute triangle edges and normal outside the loop float e0X = tri.V1.X - tri.V0.X; float e0Y = tri.V1.Y - tri.V0.Y; float e0Z = tri.V1.Z - tri.V0.Z; float e1X = tri.V2.X - tri.V1.X; float e1Y = tri.V2.Y - tri.V1.Y; float e1Z = tri.V2.Z - tri.V1.Z; float e2X = tri.V0.X - tri.V2.X; float e2Y = tri.V0.Y - tri.V2.Y; float e2Z = tri.V0.Z - tri.V2.Z; @@ -701,13 +682,11 @@ public static void Voxelize( gridOrigin.Z + (z + 0.5f) * cellSize ); - // Pass precomputed values into the intersection test if (CheckTriangleBoxIntersection(tri, cellCenter, cellSize, e0X, e0Y, e0Z, e1X, e1Y, e1Z, e2X, e2Y, e2Z, normalX, normalY, normalZ)) { int flatIndex = x + (y * gridX) + (z * gridX * gridY); - // OPTIMIZATION 2: Cache-friendly early exit prevents memory bus locking if (voxelGrid[flatIndex] != 0) { Atomic.Exchange(ref voxelGrid[flatIndex], 0); @@ -727,17 +706,14 @@ private static bool CheckTriangleBoxIntersection( { float boxHalf = cellSize * 0.5f; - // Shift triangle to local AABB coordinate space float v0X = tri.V0.X - boxCenter.X; float v0Y = tri.V0.Y - boxCenter.Y; float v0Z = tri.V0.Z - boxCenter.Z; float v1X = tri.V1.X - boxCenter.X; float v1Y = tri.V1.Y - boxCenter.Y; float v1Z = tri.V1.Z - boxCenter.Z; float v2X = tri.V2.X - boxCenter.X; float v2Y = tri.V2.Y - boxCenter.Y; float v2Z = tri.V2.Z - boxCenter.Z; - // SAT Test 1: Box AABB bounds if (Min3(v0X, v1X, v2X) > boxHalf || Max3(v0X, v1X, v2X) < -boxHalf) return false; if (Min3(v0Y, v1Y, v2Y) > boxHalf || Max3(v0Y, v1Y, v2Y) < -boxHalf) return false; if (Min3(v0Z, v1Z, v2Z) > boxHalf || Max3(v0Z, v1Z, v2Z) < -boxHalf) return false; - // SAT Test 2: Triangle Plane vs Box Overlap float d = -(normalX * v0X + normalY * v0Y + normalZ * v0Z); float vminX = normalX > 0f ? -boxHalf : boxHalf; float vmaxX = normalX > 0f ? boxHalf : -boxHalf; @@ -747,7 +723,6 @@ private static bool CheckTriangleBoxIntersection( if ((normalX * vminX + normalY * vminY + normalZ * vminZ) + d > 0f) return false; if ((normalX * vmaxX + normalY * vmaxY + normalZ * vmaxZ) + d < 0f) return false; - // SAT Test 3: Edge Cross Products if (!AxisTest(e0Z, -e0Y, v0Y, v0Z, v2Y, v2Z, boxHalf)) return false; if (!AxisTest(e1Z, -e1Y, v1Y, v1Z, v0Y, v0Z, boxHalf)) return false; if (!AxisTest(e2Z, -e2Y, v2Y, v2Z, v1Y, v1Z, boxHalf)) return false; diff --git a/Data/GameProxy.cs b/Data/GameProxy.cs index 4eac966..8fccb7e 100644 --- a/Data/GameProxy.cs +++ b/Data/GameProxy.cs @@ -173,17 +173,28 @@ private InputActions LoadInputActions() var inputActionDefinitionType = FindType("InputActionDefinition"); var actionsDir = GameFacts.GetActionsPath(this.BaseGamePath); - // Dedicated lock object prevents global thread locking issues object syncObj = new object(); - Parallel.ForEach(Directory.EnumerateFiles(actionsDir), file => + // 1. FILTER: Only process actual data files to stop blind SerializationExceptions + var validFiles = Directory.EnumerateFiles(actionsDir) + .Where(f => f.EndsWith(".sbc", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".json", StringComparison.OrdinalIgnoreCase)); + + Parallel.ForEach(validFiles, file => { try { + // 2. FAST PATH: If the file doesn't contain a Guid, it's not an input action. + // Skip it instantly to prevent VRage SerializationExceptions and RuntimeBinderExceptions. + string contentPreview = File.ReadAllText(file); + if (!contentPreview.Contains("Guid") && !contentPreview.Contains("guid")) + return; + var def = DeserializeFile(file); + if (def == null) return; + + // We know it has a Guid now, so the dynamic binder won't crash Guid id = def.Guid; - // Safely lock using the dedicated object lock (syncObj) { actions.Actions.Add(id, new InputActions.InputActionInfo @@ -196,7 +207,9 @@ private InputActions LoadInputActions() } } catch - { } + { + // Ignore any files that genuinely fail serialization + } }); var proxy = new ProxyGenerator(); @@ -237,11 +250,24 @@ private InputIds LoadInputIds() if (kind is null) continue; - var inputId = DynamicHelper.Unwrap(input.GetValue(null).AsDynamic().Id); - kind.Add(inputId); - - dynamicProvider.TryGetName(inputId, out string displayName); - inputIds.InputIdToDisplayName.Add(inputId, displayName); + try + { + var val = input.GetValue(null); + if (val == null) continue; + + // 3. SAFE REFLECTION: Check if the 'Id' property exists before touching the dynamic binder + var type = val.GetType(); + if (type.GetProperty("Id") == null && type.GetField("Id") == null) + continue; // Safely skip without throwing a RuntimeBinderException + + var inputId = DynamicHelper.Unwrap(val.AsDynamic().Id); + kind.Add(inputId); + + dynamicProvider.TryGetName(inputId, out string displayName); + inputIds.InputIdToDisplayName.Add(inputId, displayName); + } + catch + { } } } From 6afef628473d795aff2c6062cf26d364e8a1d0d4 Mon Sep 17 00:00:00 2001 From: Zemogiter Date: Mon, 27 Jul 2026 17:10:16 +0200 Subject: [PATCH 7/8] Update GridShaper.cs --- Algorithms/GridShaper.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Algorithms/GridShaper.cs b/Algorithms/GridShaper.cs index 10c98ed..d460c30 100644 --- a/Algorithms/GridShaper.cs +++ b/Algorithms/GridShaper.cs @@ -129,17 +129,6 @@ private BlueprintMesh CoreGenerate(GeneratorSettings settings, bool useGpu, Canc long totalGridVolume = (long)gridX * gridY * gridZ; - if (totalGridVolume >= 67_108_864) - { - throw new Exception($"Model is too large for {settings.BlockSize} resolution!\n" + - $"Requires {totalGridVolume:N0} blocks ({gridX}x{gridY}x{gridZ}).\n" + - $"Please scale the model down or select a larger block size."); - } - if (this.Mesh.TriangleCount == 0) - { - throw new Exception("The selected model contains no 3D geometry."); - } - var indexer = new ShiftGridIndexer3(bounds.Min, blockSize); var bmp = new DenseGrid3i(gridX, gridY, gridZ, BlueprintMesh.NoContent); int triangleCount = this.Mesh.TriangleCount; From 9dc457582671e651267903f1358a652054b8acb5 Mon Sep 17 00:00:00 2001 From: Zemogiter Date: Mon, 27 Jul 2026 23:02:44 +0200 Subject: [PATCH 8/8] GPU-accelerate slope generation in voxelization Moves slope generation to GPU execution as part of the voxelization pipeline instead of post-processing on CPU (will still be fully executed on CPU if the mode is enabled via shift key). Adds GpuSlopeParams struct to bypass C# 16-parameter limit for GPU kernels, and includes LOH compaction for memory management after voxelization. Added extra memory checks close to the end to make sure the software wont hog too much RAM. Also updates GameProxy to filter for .def files instead of .sbc, and removes the GUID pre-check optimization and unnecessary comments. --- Algorithms/GridShaper.cs | 244 ++++++++++++++++++++++++++++----------- Data/GameProxy.cs | 13 +-- 2 files changed, 176 insertions(+), 81 deletions(-) diff --git a/Algorithms/GridShaper.cs b/Algorithms/GridShaper.cs index d460c30..0d0bb1e 100644 --- a/Algorithms/GridShaper.cs +++ b/Algorithms/GridShaper.cs @@ -4,6 +4,7 @@ using PropertyTools.DataAnnotations; using SpaceEditor.Rocks; using System.IO; +using System.Runtime; using System.Text; namespace SpaceEditor.Algorithms; @@ -25,6 +26,18 @@ public struct GpuTriangle public Float3 MaxBounds; } +// Struct to bypass the C# 16-parameter limit for delegates +public struct GpuSlopeParams +{ + public int GridX, GridY, GridZ; + public int PAx, PAy, PAz; + public int PBx, PBy, PBz; + public int SAx, SAy, SAz; + public int SBx, SBy, SBz; + public int Content; + public int MustBeSupported; +} + public class GridShaper { public DMesh3 Mesh { get; } @@ -43,6 +56,7 @@ public static class GpuSetup public static Accelerator Accelerator { get; private set; } public static Action, ArrayView, Float3, float, int, int, int> VoxelizeKernel { get; private set; } public static Action, int> InitGridKernel { get; private set; } + public static Action, GpuSlopeParams> SlopeKernel { get; private set; } public static readonly object SyncLock = new object(); @@ -70,6 +84,10 @@ public static void RebuildContext() InitGridKernel = Accelerator.LoadAutoGroupedStreamKernel< Index1D, ArrayView, int>( VoxelizationKernel.InitializeGrid); + + SlopeKernel = Accelerator.LoadAutoGroupedStreamKernel< + Index1D, ArrayView, GpuSlopeParams>( + VoxelizationKernel.GenerateSlopes); } } } @@ -108,6 +126,7 @@ public BlueprintMesh Generate(GeneratorSettings settings, CancellationToken ct, public BlueprintMesh GenerateCpu(GeneratorSettings settings, CancellationToken ct, IProgress<(double, string)> progress = null) => CoreGenerate(settings, useGpu: false, ct, progress); + // --- UNIFIED GENERATOR --- private BlueprintMesh CoreGenerate(GeneratorSettings settings, bool useGpu, CancellationToken ct, IProgress<(double, string)> progress) { @@ -147,19 +166,21 @@ private BlueprintMesh CoreGenerate(GeneratorSettings settings, bool useGpu, Canc IEnumerable activeBlocks; // ========================================== - // PHASE 2: VOXELIZATION (BRANCHING) + // PHASE 2 & 3: GPU VOXELIZATION & SLOPE GENERATION // ========================================== if (useGpu) { Accelerator accelerator; Action, ArrayView, Float3, float, int, int, int> voxelizeKernel; Action, int> initGridKernel; + Action, GpuSlopeParams> slopeKernel; lock (GpuSetup.SyncLock) { accelerator = GpuSetup.Accelerator; voxelizeKernel = GpuSetup.VoxelizeKernel; initGridKernel = GpuSetup.InitGridKernel; + slopeKernel = GpuSetup.SlopeKernel; } System.Diagnostics.Debug.WriteLine($"\n[ILGPU VERIFICATION] Executing on: {accelerator.Name} (Type: {accelerator.AcceleratorType})\n"); @@ -183,7 +204,6 @@ private BlueprintMesh CoreGenerate(GeneratorSettings settings, bool useGpu, Canc }; } - int[] flatResults; int totalCells = (int)totalGridVolume; try @@ -196,45 +216,90 @@ private BlueprintMesh CoreGenerate(GeneratorSettings settings, bool useGpu, Canc Float3 origin = new Float3((float)bounds.Min.x, (float)bounds.Min.y, (float)bounds.Min.z); voxelizeKernel(deviceTriangles.IntExtent, deviceTriangles.View, deviceGrid.View, origin, blockSize, gridX, gridY, gridZ); - accelerator.Synchronize(); - progress?.Report((0.40, "Voxelization...")); - flatResults = deviceGrid.GetAsArray1D(); - } - catch (Exception ex) - { - throw new Exception("The GPU driver failed during allocation. Out of VRAM or driver error.", ex); - } + progress?.Report((0.40, "Voxelization complete on GPU...")); - _ = Task.Run(() => GpuSetup.RebuildContext()); + int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); + int executedPasses = 0; - int processedZ = 0; - var gpuActiveBlocks = new System.Collections.Concurrent.ConcurrentBag(); + void ExecGpuSlopes(int content) + { + var shapeInfo = blueprint.Shapes[content]; + var probeDirectionA = -Base6Directions.Vectors[shapeInfo.Forward]; + var probeDirectionB = Base6Directions.Vectors[shapeInfo.Up]; + var supportDirectionA = -probeDirectionA; + var supportDirectionB = -probeDirectionB; - Parallel.For(0, gridZ, new ParallelOptions { CancellationToken = ct }, z => - { - for (int y = 0; y < gridY; y++) + var slopeParams = new GpuSlopeParams + { + GridX = gridX, + GridY = gridY, + GridZ = gridZ, + PAx = probeDirectionA.x, + PAy = probeDirectionA.y, + PAz = probeDirectionA.z, + PBx = probeDirectionB.x, + PBy = probeDirectionB.y, + PBz = probeDirectionB.z, + SAx = supportDirectionA.x, + SAy = supportDirectionA.y, + SAz = supportDirectionA.z, + SBx = supportDirectionB.x, + SBy = supportDirectionB.y, + SBz = supportDirectionB.z, + Content = content, + MustBeSupported = settings.SlopesMustBeSupported ? 1 : 0 + }; + + slopeKernel(totalCells, deviceGrid.View, slopeParams); + accelerator.Synchronize(); + + if (totalSlopePasses > 0) + { + int currentPass = Interlocked.Increment(ref executedPasses); + progress?.Report((0.40 + (0.30 * ((double)currentPass / totalSlopePasses)), "GPU Slope Evaluation...")); + } + } + + if (settings.SlopesUpper) { ExecGpuSlopes(1); ExecGpuSlopes(2); ExecGpuSlopes(3); ExecGpuSlopes(4); } + if (settings.SlopesLower) { ExecGpuSlopes(5); ExecGpuSlopes(6); ExecGpuSlopes(7); ExecGpuSlopes(8); } + if (settings.SlopesSides) { ExecGpuSlopes(9); ExecGpuSlopes(10); ExecGpuSlopes(11); ExecGpuSlopes(12); } + + int[] flatResults = deviceGrid.GetAsArray1D(); + _ = Task.Run(() => GpuSetup.RebuildContext()); + + int processedZ = 0; + var gpuActiveBlocks = new System.Collections.Concurrent.ConcurrentBag(); + + Parallel.For(0, gridZ, new ParallelOptions { CancellationToken = ct }, z => { - for (int x = 0; x < gridX; x++) + for (int y = 0; y < gridY; y++) { - int flatIdx = x + (y * gridX) + (z * gridX * gridY); - if (flatResults[flatIdx] == 0) + for (int x = 0; x < gridX; x++) { - var cell = new Vector3i(x, y, z); - bmp[cell] = 0; - gpuActiveBlocks.Add(cell); + int flatIdx = x + (y * gridX) + (z * gridX * gridY); + if (flatResults[flatIdx] != BlueprintMesh.NoContent) + { + var cell = new Vector3i(x, y, z); + bmp[cell] = flatResults[flatIdx]; + gpuActiveBlocks.Add(cell); + } } } - } - int currentZ = Interlocked.Increment(ref processedZ); - if (currentZ % 10 == 0) - { - progress?.Report((0.40 + (0.30 * ((double)currentZ / gridZ)), "Grid Reconstruction...")); - } - }); + int currentZ = Interlocked.Increment(ref processedZ); + if (currentZ % 10 == 0) + { + progress?.Report((0.70 + (0.30 * ((double)currentZ / gridZ)), "Grid Reconstruction...")); + } + }); - activeBlocks = gpuActiveBlocks; + activeBlocks = gpuActiveBlocks; + } + catch (Exception ex) + { + throw new Exception("The GPU driver failed during allocation or kernel execution. Out of VRAM or driver error.", ex); + } } else { @@ -268,58 +333,64 @@ private BlueprintMesh CoreGenerate(GeneratorSettings settings, bool useGpu, Canc } activeBlocks = cpuActiveBlocks; - } - // ========================================== - // PHASE 3: SHARED SLOPE GENERATION - // ========================================== - progress?.Report((0.70, "Slope Evaluation...")); - int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); - int executedPasses = 0; + progress?.Report((0.70, "Slope Evaluation...")); + int totalSlopePasses = (settings.SlopesUpper ? 4 : 0) + (settings.SlopesLower ? 4 : 0) + (settings.SlopesSides ? 4 : 0); + int executedPasses = 0; - if (settings.SlopesUpper) { ExecSlopes(1); ExecSlopes(2); ExecSlopes(3); ExecSlopes(4); } - if (settings.SlopesLower) { ExecSlopes(5); ExecSlopes(6); ExecSlopes(7); ExecSlopes(8); } - if (settings.SlopesSides) { ExecSlopes(9); ExecSlopes(10); ExecSlopes(11); ExecSlopes(12); } + if (settings.SlopesUpper) { ExecCpuSlopes(1); ExecCpuSlopes(2); ExecCpuSlopes(3); ExecCpuSlopes(4); } + if (settings.SlopesLower) { ExecCpuSlopes(5); ExecCpuSlopes(6); ExecCpuSlopes(7); ExecCpuSlopes(8); } + if (settings.SlopesSides) { ExecCpuSlopes(9); ExecCpuSlopes(10); ExecCpuSlopes(11); ExecCpuSlopes(12); } - void ExecSlopes(int content) - { - var shapeInfo = blueprint.Shapes[content]; - var probeDirectionA = -Base6Directions.Vectors[shapeInfo.Forward]; - var probeDirectionB = Base6Directions.Vectors[shapeInfo.Up]; - var supportDirectionA = -probeDirectionA; - var supportDirectionB = -probeDirectionB; - - Parallel.ForEach(activeBlocks, new ParallelOptions { CancellationToken = ct }, g => + void ExecCpuSlopes(int content) { - if (blueprint[g] != 0) return; + var shapeInfo = blueprint.Shapes[content]; + var probeDirectionA = -Base6Directions.Vectors[shapeInfo.Forward]; + var probeDirectionB = Base6Directions.Vectors[shapeInfo.Up]; + var supportDirectionA = -probeDirectionA; + var supportDirectionB = -probeDirectionB; - if (blueprint[g + probeDirectionA] != BlueprintMesh.NoContent || blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) + Parallel.ForEach(activeBlocks, new ParallelOptions { CancellationToken = ct }, g => { - return; - } + if (blueprint[g] != 0) return; - if (settings.SlopesMustBeSupported) - { - if (blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) + if (blueprint[g + probeDirectionA] != BlueprintMesh.NoContent || blueprint[g + probeDirectionB] != BlueprintMesh.NoContent) { return; } - } - bmp[g] = content; - }); + if (settings.SlopesMustBeSupported) + { + if (blueprint[g + supportDirectionA] != 0 || blueprint[g + supportDirectionB] != 0) + { + return; + } + } - if (totalSlopePasses > 0) - { - int currentPass = Interlocked.Increment(ref executedPasses); - progress?.Report((0.70 + (0.30 * ((double)currentPass / totalSlopePasses)), "Slope Evaluation...")); + bmp[g] = content; + }); + + if (totalSlopePasses > 0) + { + int currentPass = Interlocked.Increment(ref executedPasses); + progress?.Report((0.70 + (0.30 * ((double)currentPass / totalSlopePasses)), "Slope Evaluation...")); + } } } + // Forcing the .NET runtime to compact the LOH and free up memory after the voxelization and slope generation, watch this space in case it hurts performance + GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; + GC.Collect(2, GCCollectionMode.Aggressive, blocking: true, compacting: true); + progress?.Report((1.0, "Complete, finalization!")); return blueprint; } + + // ========================================== + // PHASE 4: CPU MESHING & FINALIZATION + // (Handles VoxelSurfaceGenerator, Marching Cubes, and Text Export) + // ========================================== public class GridMesher { public static DMesh3 Mesh(BlueprintMesh blueprint) @@ -366,7 +437,6 @@ public static DMesh3 Mesh(BlueprintMesh blueprint) var correctionOffset = blueprint.Coords.CellSize / 2; MeshTransforms.Translate(cubesMesh, blueprint.Coords.Origin + correctionOffset); - var finalMesh = cubesMesh; finalMesh.AppendMesh(slopeMesh); @@ -399,10 +469,8 @@ public ShapeDB(params ShapeInfo[] shapes) public static ShapeDB LargeShapes = new ( - // Cube CubicShape("2eacbbf2-d8fb-4a78-91dc-7b492517ef97", x => x.AppendBox(Dims(LargeBlockSize))), - // Slopes SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Left, Base6Directions.Up), SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Right, Base6Directions.Up), SlopeShape("f9efcc6c-6c76-4762-bbf0-6013ec969539", LargeBlockSize, Base6Directions.Forward, Base6Directions.Up), @@ -421,10 +489,8 @@ public ShapeDB(params ShapeInfo[] shapes) public static ShapeDB MidShapes = new ( - // Cube CubicShape("632d7385-12b9-47a6-802a-a610d0cbd1e0", x => x.AppendBox(Dims(MidBlockSize))), - // Slopes SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Left, Base6Directions.Up), SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Right, Base6Directions.Up), SlopeShape("69902790-3e2d-43d2-81e4-1c0b42bc7461", MidBlockSize, Base6Directions.Forward, Base6Directions.Up), @@ -615,7 +681,6 @@ static Vector3i ToInt(Vector3d vec) public static class VoxelizationKernel { - // GPU-safe math implementations private static int Min(int a, int b) => a < b ? a : b; private static int Max(int a, int b) => a > b ? a : b; private static float Min(float a, float b) => a < b ? a : b; @@ -686,6 +751,47 @@ public static void Voxelize( } } + public static void GenerateSlopes( + Index1D index, + ArrayView voxelGrid, + GpuSlopeParams p) + { + int x = index % p.GridX; + int y = (index / p.GridX) % p.GridY; + int z = index / (p.GridX * p.GridY); + + if (voxelGrid[index] != 0) return; + + int nxA = x + p.PAx; int nyA = y + p.PAy; int nzA = z + p.PAz; + int nxB = x + p.PBx; int nyB = y + p.PBy; int nzB = z + p.PBz; + + if (nxA < 0 || nxA >= p.GridX || nyA < 0 || nyA >= p.GridY || nzA < 0 || nzA >= p.GridZ) return; + if (nxB < 0 || nxB >= p.GridX || nyB < 0 || nyB >= p.GridY || nzB < 0 || nzB >= p.GridZ) return; + + int idxA = nxA + (nyA * p.GridX) + (nzA * p.GridX * p.GridY); + int idxB = nxB + (nyB * p.GridX) + (nzB * p.GridX * p.GridY); + + if (voxelGrid[idxA] != int.MaxValue || voxelGrid[idxB] != int.MaxValue) + return; + + if (p.MustBeSupported == 1) + { + int sxA = x + p.SAx; int syA = y + p.SAy; int szA = z + p.SAz; + int sxB = x + p.SBx; int syB = y + p.SBy; int szB = z + p.SBz; + + if (sxA < 0 || sxA >= p.GridX || syA < 0 || syA >= p.GridY || szA < 0 || szA >= p.GridZ) return; + if (sxB < 0 || sxB >= p.GridX || syB < 0 || syB >= p.GridY || szB < 0 || szB >= p.GridZ) return; + + int sIdxA = sxA + (syA * p.GridX) + (szA * p.GridX * p.GridY); + int sIdxB = sxB + (syB * p.GridX) + (szB * p.GridX * p.GridY); + + if (voxelGrid[sIdxA] != 0 || voxelGrid[sIdxB] != 0) + return; + } + + Atomic.Exchange(ref voxelGrid[index], p.Content); + } + private static bool CheckTriangleBoxIntersection( GpuTriangle tri, Float3 boxCenter, float cellSize, float e0X, float e0Y, float e0Z, diff --git a/Data/GameProxy.cs b/Data/GameProxy.cs index 8fccb7e..0d8279c 100644 --- a/Data/GameProxy.cs +++ b/Data/GameProxy.cs @@ -119,7 +119,6 @@ public GameProxy(string baseGamePath) var md = st.AsDynamicType().GetInstance(mdt); md.PushContext(new[] { se2 }); - // CACHE TYPES ONCE AT STARTUP _customSerializationContextType = FindType("CustomSerializationContext"); _serializationContextType = FindType("SerializationContext"); _serializationHelperType = FindType("SerializationHelper"); @@ -144,7 +143,6 @@ public dynamic DeserializeFile(string filePath, params object[] services) public dynamic DeserializeObject(Stream content, params object[] services) { - // Use pre-cached types instead of FindType() and Enum.Parse() var typedServices = Array.CreateInstance(_customSerializationContextType, services.Length); Array.Copy(services, typedServices, services.Length); @@ -154,14 +152,12 @@ public dynamic DeserializeObject(Stream content, params object[] services) public string SerializeObject(object instance, params object[] services) { - // Use pre-cached types var typedServices = Array.CreateInstance(_customSerializationContextType, services.Length); Array.Copy(services, typedServices, services.Length); using var data = new MemoryStream(); using var sc = (IDisposable)Activator.CreateInstance(_serializationContextType, data, "NoName.txt", typedServices)!; - // Use pre-cached MethodInfo _serializeAbstractMethod.Invoke(null, [sc, instance, _jsonFormatEnumValue]); return Encoding.UTF8.GetString(data.GetBuffer().AsSpan()[..(int)data.Length]); @@ -176,19 +172,12 @@ private InputActions LoadInputActions() object syncObj = new object(); // 1. FILTER: Only process actual data files to stop blind SerializationExceptions - var validFiles = Directory.EnumerateFiles(actionsDir) - .Where(f => f.EndsWith(".sbc", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".json", StringComparison.OrdinalIgnoreCase)); + var validFiles = Directory.EnumerateFiles(actionsDir).Where(f => f.EndsWith(".def", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".json", StringComparison.OrdinalIgnoreCase)); Parallel.ForEach(validFiles, file => { try { - // 2. FAST PATH: If the file doesn't contain a Guid, it's not an input action. - // Skip it instantly to prevent VRage SerializationExceptions and RuntimeBinderExceptions. - string contentPreview = File.ReadAllText(file); - if (!contentPreview.Contains("Guid") && !contentPreview.Contains("guid")) - return; - var def = DeserializeFile(file); if (def == null) return;