diff --git a/PDTools.Files/Textures/PS2/TextureSetBuilder.cs b/PDTools.Files/Textures/PS2/TextureSetBuilder.cs index 2868c1d..a11844c 100644 --- a/PDTools.Files/Textures/PS2/TextureSetBuilder.cs +++ b/PDTools.Files/Textures/PS2/TextureSetBuilder.cs @@ -32,12 +32,13 @@ public class TextureSetBuilder private readonly List _textures = []; /* Used to keep track of GS blocks without texture data allocated - * So that we can put other textures's data in them */ - private readonly Dictionary _unusedGsBlocksIndices = []; + * So that we can put ot private readonly SortedDictionary _unusedGsBlocksIndices = new(); /* Used to keep track of all GS blocks we've used up */ private readonly List _usedGsBlocksIndices = []; + private Dictionary _variationPaletteCache = new(); + private int _lastFreeVerticalBlock = -1; private ushort _tbp_Textures = 0; @@ -73,9 +74,6 @@ private void AddImage(Image img, TextureConfig config) { _logger?.LogInformation("Adding image {x}x{y}, format={format}", img.Width, img.Height, config.Format); - if (config.IsTextureMap) - img.Mutate(e => e.Resize((int)BitOperations.RoundUpToPowerOf2((uint)img.Width), (int)BitOperations.RoundUpToPowerOf2((uint)img.Height))); - var pgluTexture = new PGLUtexture(); pgluTexture.tex0.PSM = config.Format; @@ -214,7 +212,7 @@ public void AddClutPatch(int clutPatchSetIndex, int pgluTextureIndex, string pat else throw new Exception($"Texture file '{path}' must use less than {paletteSize} colors ahead of time."); - ClutPatchTask clutPatch = new ClutPatchTask(fullPalette); + ClutPatchTask clutPatch = new ClutPatchTask(fullPalette, (ushort)pgluTextureIndex); while (_clutPatchSets.Count <= clutPatchSetIndex) _clutPatchSets.Add([]); @@ -234,6 +232,20 @@ public TextureSet1 Build() WriteClutPatches(); + // Sometimes this information is useful to debug changes to the Tex1 Optimization + Console.WriteLine("--- Tex1 ALLOCATION MAP ---"); + foreach (var t in _textures) + { + Console.WriteLine($"Format: {t.PGLUTexture.tex0.PSM} | Size: {t.Image.Width}x{t.Image.Height} | Blocks Used: {t.SizeInGSBlocks} | TBP: {t.PGLUTexture.tex0.TBP0_TextureBaseAddress}"); + } + foreach (var p in _texSet.pgluTextures) + { + if (p.tex0.CBP_ClutBlockPointer > 0) + Console.WriteLine($"Palette Format: {p.tex0.PSM} | CBP: {p.tex0.CBP_ClutBlockPointer} | CSA: {p.tex0.CSA_ClutEntryOffset}"); + } + Console.WriteLine($"Peak Allocation (Max Block): {GetPeakAllocation()}"); + Console.WriteLine("---------------------------"); + bool swizzle = _textures.Count >= 2; if (swizzle) BuildSwizzledTransfers(); @@ -284,258 +296,333 @@ private void WritePalettes() /// /// /// - private (ushort CBP, byte CSA) FitPaletteToGSMemory(SCE_GS_PSM textureFormat, int width, int height, Rgba32[] palette, bool reuseOldPaletteLocations = false) - { - if (reuseOldPaletteLocations) + private (ushort CBP, byte CSA) FitPaletteToGSMemory(SCE_GS_PSM textureFormat, int width, int height, Rgba32[] palette, bool reuseOldPaletteLocations = false) + { + string paletteHash = string.Empty; + + if (reuseOldPaletteLocations) + { + // 1. Check Base Textures (Original Logic) + for (int i = 0; i < _texSet.pgluTextures.Count; i++) + { + PGLUtexture pgluTexture = _texSet.pgluTextures[i]; + if (pgluTexture.tex0.PSM == textureFormat && _textures[i].Palette != null && _textures[i].Palette.AsSpan().SequenceEqual(palette)) + { + return (pgluTexture.tex0.CBP_ClutBlockPointer, pgluTexture.tex0.CSA_ClutEntryOffset); + } + } + + // 2. Check Previously Allocated Variation Palettes (The 10% Saver) + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + var bytes = MemoryMarshal.AsBytes(palette.AsSpan()).ToArray(); + paletteHash = textureFormat.ToString() + "_" + BitConverter.ToString(System.Security.Cryptography.MD5.HashData(bytes)); + } + + if (_variationPaletteCache.TryGetValue(paletteHash, out var cachedLocation)) + { + return cachedLocation; // Found an exact match from a previous variation + } + } + + List usedBlocksOfTexture = GSPixelFormat.PSM_CT32.GetUsedBlocks(width, height); + int size = Tex1Utils.GetDataSize(width, height, SCE_GS_PSM.SCE_GS_PSMCT32); + int csa = 0; + byte csaTakenSpace = (byte)Math.Min(size / 32, 8); + int idx = CanFitBlocksInUnusedBlocks(usedBlocksOfTexture, csaTakenSpace); + + ushort cbp; + if (idx != -1) + { + cbp = (ushort)idx; + if (usedBlocksOfTexture.Count == 1) + { + // Fix 1: Read the block, update it, and WRITE IT BACK + GSBlock block = _unusedGsBlocksIndices[idx + usedBlocksOfTexture[0]]; + csa = block.CurrentCSA; + + block.CurrentCSA += csaTakenSpace; + + if (block.CurrentCSA >= 8) + { + _unusedGsBlocksIndices.Remove(block.Index); + _usedGsBlocksIndices.Add((ushort)block.Index); + } + else + { + // CRITICAL C# FIX: Save the modified struct back to the dictionary + _unusedGsBlocksIndices[block.Index] = block; + } + } + else + { + for (int i = 0; i < usedBlocksOfTexture.Count; i++) + { + _unusedGsBlocksIndices.Remove((ushort)(idx + usedBlocksOfTexture[i])); + _usedGsBlocksIndices.Add((ushort)(idx + usedBlocksOfTexture[i])); + } + } + } + else + { + cbp = _tbp_Textures; // Lock in the CBP BEFORE incrementing TBP + _tbp_Textures += (ushort)usedBlocksOfTexture.Count; + + if (usedBlocksOfTexture.Count == 1) + { + csa = 0; // Starts at 0 for a brand new block + + // Fix 2: Track the CBP we just used, not the freshly incremented TBP! + GSBlock partialFilledBlock = new GSBlock(cbp, csaTakenSpace); + _unusedGsBlocksIndices.Add(partialFilledBlock.Index, partialFilledBlock); + } + else + { + for (int i = 0; i < usedBlocksOfTexture.Count; i++) + _usedGsBlocksIndices.Add((ushort)(cbp + usedBlocksOfTexture[i])); + } + } + + switch (textureFormat) + { + case SCE_GS_PSM.SCE_GS_PSMT8: + _gsMemory.WriteTexPSMCT32(cbp, 1, 0, 0, 16, 16, MemoryMarshal.Cast(palette), csa * 32); + break; + case SCE_GS_PSM.SCE_GS_PSMT4: + _gsMemory.WriteTexPSMCT32(cbp, 1, 0, 0, 8, 2, MemoryMarshal.Cast(palette), csa * 32); + break; + } + + if (reuseOldPaletteLocations && !string.IsNullOrEmpty(paletteHash)) { - for (int i = 0; i < _texSet.pgluTextures.Count; i++) - { - // Is there an identical palette somewhere already? - PGLUtexture pgluTexture = _texSet.pgluTextures[i]; - if (pgluTexture.tex0.PSM == textureFormat && _textures[i].Palette.AsSpan().SequenceEqual(palette)) - { - // return its cbp - Save on size - return (pgluTexture.tex0.CBP_ClutBlockPointer, pgluTexture.tex0.CSA_ClutEntryOffset); - } - } - } - - /* The game cheats a bit with "CSA" - is it even used for its original purpose? - * "CSA" here is used as an offset WITHIN the block itself - * So a block (256 bytes) can store 4 PSMT4 palettes (64 * 4) - * CSA goes every 32 */ - - List usedBlocksOfTexture = GSPixelFormat.PSM_CT32.GetUsedBlocks(width, height); - int size = Tex1Utils.GetDataSize(width, height, SCE_GS_PSM.SCE_GS_PSMCT32); - int csa = 0; - byte csaTakenSpace = (byte)Math.Min(size / 32, 8); - int idx = CanFitBlocksInUnusedBlocks(usedBlocksOfTexture, csaTakenSpace); - - ushort cbp; - if (idx != -1) - { - cbp = (ushort)idx; - - // Is this a palette that fits in one singular block? - if (usedBlocksOfTexture.Count == 1) - { - GSBlock block = _unusedGsBlocksIndices[idx + usedBlocksOfTexture[0]]; - csa = block.CurrentCSA; - - block.CurrentCSA += csaTakenSpace; - if (block.CurrentCSA >= 8) - { - // Block CSA is 8 (32 * 8 = 256 bytes). This block is filled, move on - _unusedGsBlocksIndices.Remove(block.Index); - _usedGsBlocksIndices.Add((ushort)block.Index); - _tbp_Textures++; - } - } - else - { - for (int i = 0; i < usedBlocksOfTexture.Count; i++) - { - _unusedGsBlocksIndices.Remove((ushort)(idx + usedBlocksOfTexture[i])); - _usedGsBlocksIndices.Add((ushort)(idx + usedBlocksOfTexture[i])); - } - } - } - else - { - cbp = _tbp_Textures; - _tbp_Textures += (ushort)usedBlocksOfTexture.Count; - - if (usedBlocksOfTexture.Count == 1) - { - csa = csaTakenSpace; - - GSBlock partialFilledBlock = new GSBlock(_tbp_Textures + usedBlocksOfTexture[0], csaTakenSpace); - _unusedGsBlocksIndices.Add(partialFilledBlock.Index, partialFilledBlock); - } - else - { - for (int i = 0; i < usedBlocksOfTexture.Count; i++) - _usedGsBlocksIndices.Add((ushort)(_tbp_Textures + usedBlocksOfTexture[i])); - } - } - - switch (textureFormat) - { - case SCE_GS_PSM.SCE_GS_PSMT8: - _gsMemory.WriteTexPSMCT32(cbp, 1, - 0, 0, - 16, 16, - MemoryMarshal.Cast(palette), - csa * 32); - break; - - case SCE_GS_PSM.SCE_GS_PSMT4: - _gsMemory.WriteTexPSMCT32(cbp, 1, - 0, 0, - 8, 2, - MemoryMarshal.Cast(palette), - csa * 32); - break; + _variationPaletteCache[paletteHash] = (cbp, (byte)csa); } return (cbp, (byte)csa); - } - - private void WriteClutPatches() - { - if (_texSet.ClutPatchSet.Count < 1) - return; - - for (ushort i = 0; i < _texSet.pgluTextures.Count; i++) - { - var clutPatch = _texSet.ClutPatchSet[0].TexturesToPatch[i]; - var pgluTexture= _texSet.pgluTextures[i]; - - clutPatch.CBP_ClutBufferBasePointer = pgluTexture.tex0.CBP_ClutBlockPointer; - clutPatch.CSA_ClutEntryOffset = pgluTexture.tex0.CSA_ClutEntryOffset; - clutPatch.PGLUTextureIndex = i; - clutPatch.Format = SCE_GS_PSM.SCE_GS_PSMCT32; - } - - for (int varIndex = 1; varIndex < _clutPatchSets.Count; varIndex++) - { - var clutPatchSet = new ClutPatchSet(); - _texSet.ClutPatchSet.Add(clutPatchSet); - - for (ushort textureIndex = 0; textureIndex < _clutPatchSets[varIndex].Count; textureIndex++) - { - ClutPatchTask clutPatchTask = _clutPatchSets[varIndex][textureIndex]; - - SCE_GS_PSM textureFormat = _textures[textureIndex].PGLUTexture.tex0.PSM; - int width = textureFormat == SCE_GS_PSM.SCE_GS_PSMT8 ? 16 : 8; - int height = textureFormat == SCE_GS_PSM.SCE_GS_PSMT8 ? 16 : 2; - - var clutPatch = new TextureClutPatch(); - (ushort CBP, byte CSA) = FitPaletteToGSMemory(textureFormat, width, height, clutPatchTask.Palette, reuseOldPaletteLocations: true); - clutPatch.CBP_ClutBufferBasePointer = CBP; - clutPatch.CSA_ClutEntryOffset = CSA; - clutPatch.PGLUTextureIndex = textureIndex; - clutPatch.Format = SCE_GS_PSM.SCE_GS_PSMCT32; - - clutPatchSet.TexturesToPatch.Add(clutPatch); - } - } - } + } + + private void WriteClutPatches() + { + if (_clutPatchSets.Count == 0) + return; + + // 1. Find all texture indices that are patched in ANY variation + // Using a SortedSet ensures the array order is perfectly identical across all variations + var patchedTextureIndices = new SortedSet(); + for (int v = 1; v < _clutPatchSets.Count; v++) + { + foreach (var task in _clutPatchSets[v]) + patchedTextureIndices.Add(task.TargetTextureIndex); + } + + // 2. Base Variation (0): Establish the baseline array + foreach (ushort targetIndex in patchedTextureIndices) + { + var pgluTexture = _texSet.pgluTextures[targetIndex]; + + var clutPatch = new TextureClutPatch + { + CBP_ClutBufferBasePointer = pgluTexture.tex0.CBP_ClutBlockPointer, + CSA_ClutEntryOffset = pgluTexture.tex0.CSA_ClutEntryOffset, + PGLUTextureIndex = targetIndex, + Format = SCE_GS_PSM.SCE_GS_PSMCT32 + }; + _texSet.ClutPatchSet[0].TexturesToPatch.Add(clutPatch); + } + + // 3. Subsequent Variations: Must perfectly mirror the Base Variation array length and order + for (int varIndex = 1; varIndex < _clutPatchSets.Count; varIndex++) + { + var clutPatchSet = new ClutPatchSet(); + _texSet.ClutPatchSet.Add(clutPatchSet); + + // Quick lookup dictionary for the tasks present in this specific variation + var tasksForThisVariation = _clutPatchSets[varIndex].ToDictionary(t => t.TargetTextureIndex); + + // Iterate over the exact same sorted baseline indices + foreach (ushort targetIndex in patchedTextureIndices) + { + var clutPatch = new TextureClutPatch + { + PGLUTextureIndex = targetIndex, + Format = SCE_GS_PSM.SCE_GS_PSMCT32 + }; + + if (tasksForThisVariation.TryGetValue(targetIndex, out var clutPatchTask)) + { + // This variation ACTIVELY changes this texture. Write the new palette to VRAM. + SCE_GS_PSM textureFormat = _textures[targetIndex].PGLUTexture.tex0.PSM; + int width = textureFormat == SCE_GS_PSM.SCE_GS_PSMT8 ? 16 : 8; + int height = textureFormat == SCE_GS_PSM.SCE_GS_PSMT8 ? 16 : 2; + + (ushort CBP, byte CSA) = FitPaletteToGSMemory(textureFormat, width, height, clutPatchTask.Palette, reuseOldPaletteLocations: true); + + clutPatch.CBP_ClutBufferBasePointer = CBP; + clutPatch.CSA_ClutEntryOffset = CSA; + } + else + { + // This variation DOES NOT change this texture. + // Generate a Ghost Patch pointing to the base palette to prevent index desync. + var baseTexture = _texSet.pgluTextures[targetIndex]; + clutPatch.CBP_ClutBufferBasePointer = baseTexture.tex0.CBP_ClutBlockPointer; + clutPatch.CSA_ClutEntryOffset = baseTexture.tex0.CSA_ClutEntryOffset; + } + + clutPatchSet.TexturesToPatch.Add(clutPatch); + } + } + } /// /// Fits all the textures to the emulated GS memory in a block-optimized way and updates their block pointers. /// /// /// - private void WriteTexturesOptimized() - { - /* Order by textures that allocates the most GS blocks without actually using them - * That way we can put texture data in there - - * There's still some good improvements that can be made here for certain - as i am only trying to fit the rest of the textures where they can fit - instead of arranging them to begin with - */ - - var texturesByUnusedGsBlocks = _textures.OrderByDescending(e => e.SizeInGSBlocks); - foreach (TextureTask texture in texturesByUnusedGsBlocks) - { - List usedBlocksOfTexture = texture.TexturePixelFormat.GetUsedBlocks(texture.Image.Width, texture.Image.Height); - - // #1: Start searching if we can fit the texture in all unused GS blocks. - int unusedBlockFitIndex = CanFitBlocksInUnusedBlocks(usedBlocksOfTexture); - if (unusedBlockFitIndex != -1) - { - // We were able to fit the texture in unused blocks - for (int i = 0; i < usedBlocksOfTexture.Count; i++) - { - if (_unusedGsBlocksIndices.ContainsKey((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i]))) - _unusedGsBlocksIndices.Remove((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i])); - - _usedGsBlocksIndices.Add((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i])); - } - - texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = (ushort)unusedBlockFitIndex; - WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, - texture.Image.Width, texture.Image.Height, - texture.PGLUTexture.tex0.PSM, - texture.PackedImageData); - continue; - } - - // #2: Check if we can fit the texture after the last vertical row (provided the page layout is ok?) - int afterLastRowFitBlockIdx = -1; - if (_tbp_Textures != 0 && _lastFreeVerticalBlock != -1) - { - for (ushort blockIdx = (ushort)_lastFreeVerticalBlock; blockIdx < _tbp_Textures; blockIdx++) - { - int j = 0; - for (j = 0; j < usedBlocksOfTexture.Count; j++) - { - if (_usedGsBlocksIndices.Contains((ushort)(blockIdx + usedBlocksOfTexture[j]))) - { - // Starting block index is not suitable to fit the texture, move to next one - break; - } - } - - if (j == usedBlocksOfTexture.Count) - { - afterLastRowFitBlockIdx = blockIdx; - break; - } - } - - if (afterLastRowFitBlockIdx != -1) - { - for (int i = 0; i < usedBlocksOfTexture.Count; i++) - { - if (_unusedGsBlocksIndices.ContainsKey((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i]))) - _unusedGsBlocksIndices.Remove((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i])); - - _usedGsBlocksIndices.Add((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i])); - } - - texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = (ushort)afterLastRowFitBlockIdx; - _lastFreeVerticalBlock = (ushort)(afterLastRowFitBlockIdx + texture.FirstFreeVerticalBlock); - - _tbp_Textures = _usedGsBlocksIndices.Max(e => e); - WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, - texture.Image.Width, texture.Image.Height, - texture.PGLUTexture.tex0.PSM, - texture.PackedImageData); - continue; - } - } - - - // Unable to fit anywhere (it seems). We are allocating new blocks starting from tbp - texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = _tbp_Textures; - _lastFreeVerticalBlock = (ushort)(_tbp_Textures + texture.FirstFreeVerticalBlock); - - for (int i = 0; i < texture.UnusedGSBlocks.Count; i++) - { - ushort idx = (ushort)(_tbp_Textures + texture.UnusedGSBlocks[i]); - _unusedGsBlocksIndices.Add(idx, new GSBlock(idx, 0)); - } - - for (int j = 0; j < usedBlocksOfTexture.Count; j++) - _usedGsBlocksIndices.Add((ushort)(_tbp_Textures + usedBlocksOfTexture[j])); - - if (_tbp_Textures + texture.SizeInGSBlocks >= GSMemory.MAX_BLOCKS) - throw new OutOfMemoryException($"Textures take more space than the maximum GS memory capacity ({_tbp_Textures + texture.SizeInGSBlocks} >= {GSMemory.MAX_BLOCKS})."); - - WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, - texture.Image.Width, texture.Image.Height, - texture.PGLUTexture.tex0.PSM, - texture.PackedImageData); - + + + // We are going to intercept the textures before they ever touch the emulated GS Memory. + // If the pixel indices match an image we have already processed, we point the texture header + // to the existing TBP and skip allocating new blocks entirely + private void WriteTexturesOptimized() + { + /* Order by textures that allocates the most GS blocks without actually using them + * That way we can put texture data in there + * There's still some good improvements that can be made here for certain + as i am only trying to fit the rest of the textures where they can fit + instead of arranging them to begin with + */ + + var texturesOptimized = _textures + // Group formats to keep GS page matrices aligned + .OrderByDescending(t => t.PGLUTexture.tex0.PSM) + // Anchor the massive liveries first so they stack perfectly flush + .ThenByDescending(t => t.SizeInGSBlocks) + .ToList(); + + // Cache to track which pixel arrays are already in VRAM + var allocatedTBPs = new Dictionary(); + + foreach (TextureTask texture in texturesOptimized) + { + // Generate the unique, format-aware hash key for this texture + var cacheKey = new TextureCacheKey( + texture.PGLUTexture.tex0.PSM, + texture.Image.Width, + texture.Image.Height, + texture.PackedImageData + ); + + // --- TBP Deduplication Check --- + if (allocatedTBPs.TryGetValue(cacheKey, out ushort existingTbp)) + { + _logger?.LogInformation("Deduplicating Texture Data. Sharing TBP: {tbp}", existingTbp); + texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = existingTbp; + continue; + } + + List usedBlocksOfTexture = texture.TexturePixelFormat.GetUsedBlocks(texture.Image.Width, texture.Image.Height); + + // #1: Start searching if we can fit the texture in all unused GS blocks. + int unusedBlockFitIndex = CanFitBlocksInUnusedBlocks(usedBlocksOfTexture); + if (unusedBlockFitIndex != -1) + { + // We were able to fit the texture in unused blocks + for (int i = 0; i < usedBlocksOfTexture.Count; i++) + { + if (_unusedGsBlocksIndices.ContainsKey((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i]))) + _unusedGsBlocksIndices.Remove((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i])); + + _usedGsBlocksIndices.Add((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i])); + } + + texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = (ushort)unusedBlockFitIndex; + WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, + texture.Image.Width, texture.Image.Height, + texture.PGLUTexture.tex0.PSM, + texture.PackedImageData); + + // Cache the successfully written texture + allocatedTBPs[cacheKey] = texture.PGLUTexture.tex0.TBP0_TextureBaseAddress; + continue; + } + + // #2: Check if we can fit the texture after the last vertical row (provided the page layout is ok?) + int afterLastRowFitBlockIdx = -1; + if (_tbp_Textures != 0 && _lastFreeVerticalBlock != -1) + { + for (ushort blockIdx = (ushort)_lastFreeVerticalBlock; blockIdx < _tbp_Textures; blockIdx++) + { + int j = 0; + for (j = 0; j < usedBlocksOfTexture.Count; j++) + { + if (_usedGsBlocksIndices.Contains((ushort)(blockIdx + usedBlocksOfTexture[j]))) + { + // Starting block index is not suitable to fit the texture, move to next one + break; + } + } + + if (j == usedBlocksOfTexture.Count) + { + afterLastRowFitBlockIdx = blockIdx; + break; + } + } + + if (afterLastRowFitBlockIdx != -1) + { + for (int i = 0; i < usedBlocksOfTexture.Count; i++) + { + if (_unusedGsBlocksIndices.ContainsKey((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i]))) + _unusedGsBlocksIndices.Remove((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i])); + + _usedGsBlocksIndices.Add((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i])); + } + + texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = (ushort)afterLastRowFitBlockIdx; + _lastFreeVerticalBlock = (ushort)(afterLastRowFitBlockIdx + texture.FirstFreeVerticalBlock); + + _tbp_Textures = _usedGsBlocksIndices.Max(e => e); + WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, + texture.Image.Width, texture.Image.Height, + texture.PGLUTexture.tex0.PSM, + texture.PackedImageData); + + // Cache the successfully written texture + allocatedTBPs[cacheKey] = texture.PGLUTexture.tex0.TBP0_TextureBaseAddress; + continue; + } + } + + // Unable to fit anywhere (it seems). We are allocating new blocks starting from tbp + texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = _tbp_Textures; + _lastFreeVerticalBlock = (ushort)(_tbp_Textures + texture.FirstFreeVerticalBlock); + + for (int i = 0; i < texture.UnusedGSBlocks.Count; i++) + { + ushort idx = (ushort)(_tbp_Textures + texture.UnusedGSBlocks[i]); + _unusedGsBlocksIndices.Add(idx, new GSBlock(idx, 0)); + } + + for (int j = 0; j < usedBlocksOfTexture.Count; j++) + _usedGsBlocksIndices.Add((ushort)(_tbp_Textures + usedBlocksOfTexture[j])); + + if (_tbp_Textures + texture.SizeInGSBlocks >= GSMemory.MAX_BLOCKS) + throw new OutOfMemoryException($"Textures take more space than the maximum GS memory capacity ({_tbp_Textures + texture.SizeInGSBlocks} >= {GSMemory.MAX_BLOCKS})."); + + WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, + texture.Image.Width, texture.Image.Height, + texture.PGLUTexture.tex0.PSM, + texture.PackedImageData); + + // Cache the successfully written texture + allocatedTBPs[cacheKey] = texture.PGLUTexture.tex0.TBP0_TextureBaseAddress; + uint textureTbp = texture.SizeInGSBlocks; - if (textureTbp == 1) - textureTbp = 4; _tbp_Textures += (ushort)textureTbp; - } - } + } + } private void BuildTransfers() { @@ -564,28 +651,55 @@ private void BuildTransfers() } } - private void BuildSwizzledTransfers() - { - int lastUsedBlock = _usedGsBlocksIndices.Max(e => e); - - // Make sure we calculate (and align) the size from the blocks instead since we're swizzling - var transferSizes = Tex1Utils.CalculateSwizzledTransferSizes(lastUsedBlock * GSMemory.BLOCK_SIZE_BYTES); - - int tbp = 0; - foreach (var (Width, Height) in transferSizes) - { - _logger?.LogDebug("Adding swizzled transfer {x}x{y}, tbp={tbp}", Width, Height, tbp); - - byte[] transferData = new byte[Width * Height * 4]; - _gsMemory.ReadTexPSMCT32(tbp, 1, - 0, 0, - Width, Height, - MemoryMarshal.Cast(transferData)); - AddTransfer(GSPixelFormat.PSM_CT32, (ushort)tbp, 1, (ushort)Width, (ushort)Height, transferData); - - tbp += transferData.Length / GSMemory.BLOCK_SIZE_BYTES; - } - } + private void BuildSwizzledTransfers() + { + // 1. Calculate the TRUE physical boundary of the VRAM + int maxTextureBlock = 0; + foreach (var t in _textures) + { + int endBlock = t.PGLUTexture.tex0.TBP0_TextureBaseAddress + t.SizeInGSBlocks; + if (endBlock > maxTextureBlock) maxTextureBlock = endBlock; + } + + int maxPaletteBlock = 0; + foreach (var p in _texSet.pgluTextures) + { + int endBlock = p.tex0.CBP_ClutBlockPointer + 1; + if (endBlock > maxPaletteBlock) maxPaletteBlock = endBlock; + } + + // Check the Variation Palettes --- + int maxPatchBlock = 0; + foreach (var patchSet in _texSet.ClutPatchSet) + { + foreach (var patch in patchSet.TexturesToPatch) + { + int endBlock = patch.CBP_ClutBufferBasePointer + 1; + if (endBlock > maxPatchBlock) maxPatchBlock = endBlock; + } + } + + // The true required footprint encompasses textures, base palettes, and patch palettes + int lastUsedBlock = GetPeakAllocation(); + + // Make sure we calculate and align the size from the blocks instead since we're swizzling + var transferSizes = Tex1Utils.CalculateSwizzledTransferSizes(lastUsedBlock * GSMemory.BLOCK_SIZE_BYTES); + + int tbp = 0; + foreach (var (Width, Height) in transferSizes) + { + _logger?.LogDebug("Adding swizzled transfer {x}x{y}, tbp={tbp}", Width, Height, tbp); + + byte[] transferData = new byte[Width * Height * 4]; + _gsMemory.ReadTexPSMCT32(tbp, 1, + 0, 0, + Width, Height, + MemoryMarshal.Cast(transferData)); + AddTransfer(GSPixelFormat.PSM_CT32, (ushort)tbp, 1, (ushort)Width, (ushort)Height, transferData); + + tbp += transferData.Length / GSMemory.BLOCK_SIZE_BYTES; + } + } private static bool ImageFitsColorPalette(Image img, int paletteSize, out List colorPalette) { @@ -613,38 +727,35 @@ private static bool ImageFitsColorPalette(Image img, int paletteSize, ou /// Blocks to fit /// CSA to fit in a block /// Block index start. -1 if it could not be fitted. - private int CanFitBlocksInUnusedBlocks(List usedBlocksOfTexture, int csa = 8) - { - int unusedBlockFitIndex = -1; - - foreach (GSBlock block in _unusedGsBlocksIndices.Values) - { - // Special case when a texture/palette fits into a single block where we can - // tweak the csa register to point to it - if (usedBlocksOfTexture.Count == 1 && block.CurrentCSA + csa <= 8) - { - // We can reuse a partially filled block using CSA - return block.Index; - } - - int j = 0; - for (j = 0; j < usedBlocksOfTexture.Count; j++) - { - int blockIdx = block.Index + usedBlocksOfTexture[j]; - - if (!_unusedGsBlocksIndices.ContainsKey((ushort)blockIdx)) - break; - } - - if (j == usedBlocksOfTexture.Count) - { - unusedBlockFitIndex = block.Index; - break; - } - } - - return unusedBlockFitIndex; - } + private int CanFitBlocksInUnusedBlocks(List usedBlocksOfTexture, int csa = 8) + { + int unusedBlockFitIndex = -1; + + // Because this is a SortedDictionary, it now strictly evaluates the lowest memory addresses first. + foreach (var kvp in _unusedGsBlocksIndices) + { + GSBlock block = kvp.Value; + + if (usedBlocksOfTexture.Count == 1 && block.CurrentCSA + csa <= 8) + return block.Index; + + int j = 0; + for (j = 0; j < usedBlocksOfTexture.Count; j++) + { + int blockIdx = block.Index + usedBlocksOfTexture[j]; + if (!_unusedGsBlocksIndices.ContainsKey((ushort)blockIdx)) + break; // Gap is too small, collision detected + } + + if (j == usedBlocksOfTexture.Count) + { + unusedBlockFitIndex = block.Index; + break; // Found the lowest possible gap that fits + } + } + + return unusedBlockFitIndex; + } /// /// Creates image data for the specified texture. @@ -783,6 +894,34 @@ public static (Rgba32[] TiledPalette, int[] LinearToTiledPaletteIndices) MakeTil return (outpal, indices); } + + private int GetPeakAllocation() + { + int maxBlock = 0; + + foreach (var t in _textures) + { + int endBlock = t.PGLUTexture.tex0.TBP0_TextureBaseAddress + t.SizeInGSBlocks; + if (endBlock > maxBlock) maxBlock = endBlock; + } + + foreach (var p in _texSet.pgluTextures) + { + int endBlock = p.tex0.CBP_ClutBlockPointer + 1; + if (endBlock > maxBlock) maxBlock = endBlock; + } + + foreach (var patchSet in _texSet.ClutPatchSet) + { + foreach (var patch in patchSet.TexturesToPatch) + { + int endBlock = patch.CBP_ClutBufferBasePointer + 1; + if (endBlock > maxBlock) maxBlock = endBlock; + } + } + + return maxBlock; + } } public class TextureTask @@ -835,10 +974,14 @@ public class TextureTask public class ClutPatchTask { public Rgba32[] Palette { get; set; } + + // The new property that ties this palette to a specific texture index + public ushort TargetTextureIndex { get; set; } - public ClutPatchTask(Rgba32[] palette) + public ClutPatchTask(Rgba32[] palette, ushort targetTextureIndex) { Palette = palette; + TargetTextureIndex = targetTextureIndex; } } @@ -853,3 +996,35 @@ public GSBlock(int index, byte currentCSA) CurrentCSA = currentCSA; } } + +public readonly struct TextureCacheKey : IEquatable +{ + public SCE_GS_PSM Format { get; } + public int Width { get; } + public int Height { get; } + public string DataHash { get; } + + public TextureCacheKey(SCE_GS_PSM format, int width, int height, byte[] data) + { + Format = format; + Width = width; + Height = height; + + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + DataHash = BitConverter.ToString(md5.ComputeHash(data)); + } + } + + public bool Equals(TextureCacheKey other) + { + return Format == other.Format && + Width == other.Width && + Height == other.Height && + DataHash == other.DataHash; + } + + public override bool Equals(object obj) => obj is TextureCacheKey other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Format, Width, Height, DataHash); +} \ No newline at end of file diff --git a/PDTools.Files/Textures/PS2/TextureSetPS2Base.cs b/PDTools.Files/Textures/PS2/TextureSetPS2Base.cs index 868a81b..a6a17b9 100644 --- a/PDTools.Files/Textures/PS2/TextureSetPS2Base.cs +++ b/PDTools.Files/Textures/PS2/TextureSetPS2Base.cs @@ -16,7 +16,7 @@ namespace PDTools.Files.Textures.PS2; public abstract class TextureSetPS2Base { protected GSMemory _gsMemory = new(); - protected byte[] _inputData; + protected byte[] _inputData; // TextureSet1.cs uses this, may need refactor public ushort TotalBlockSize { get; set; } public List pgluTextures { get; set; } = []; @@ -68,8 +68,9 @@ protected Image GetImageData(PGLUtexture texture, TextureClutPatch textu if (_gsMemory is null) throw new Exception("Not input mode"); - int fullWidth = (int)Math.Pow(2, texture.tex0.TW_TextureWidth); - int fullHeight = (int)Math.Pow(2, texture.tex0.TH_TextureHeight); + // faster and cleaner to use standard C# bit-shifting + int fullWidth = 1 << texture.tex0.TW_TextureWidth; + int fullHeight = 1 << texture.tex0.TH_TextureHeight; byte[] textureData; uint[] palette = null; @@ -105,17 +106,15 @@ protected Image GetImageData(PGLUtexture texture, TextureClutPatch textu palette, (csa * 32)); break; - case SCE_GS_PSM.SCE_GS_PSMCT16: // TODO: this doesn't work properly when csa > 0 - ushort[] palette16 = new ushort[8 * 2]; - _gsMemory.ReadTexPSMCT16(cbp, - 1, - 0, 0, - 8, 2, // Always 8x2 for PSMT4 - palette16, - csa * 32); + case SCE_GS_PSM.SCE_GS_PSMCT16: + ushort[] palette16_T4 = new ushort[16]; + + // Use CSA * 32 to offset the VRAM read, but read into index 0 of palette16_T4 + _gsMemory.ReadTexPSMCT16(cbp, 1, 0, 0, 8, 2, palette16_T4, csa * 32); Console.WriteLine("Warning: CSA > 0 not properly supported for PSMCT16 yet"); - PSMCT16To32(palette, palette16); + // Convert directly + PSMCT16To32(palette, palette16_T4); break; default: throw new NotImplementedException($"Invalid or not supported palette format {texture.tex0.CPSM_ClutPartPixelFormatSetup}"); @@ -144,17 +143,14 @@ protected Image GetImageData(PGLUtexture texture, TextureClutPatch textu csa * 32); break; case SCE_GS_PSM.SCE_GS_PSMCT16: - ushort[] palette16 = new ushort[16 * 16]; + ushort[] palette16_T8 = new ushort[256]; - _gsMemory.ReadTexPSMCT16(cbp, - 1, - 0, 0, - 8, 2, // Always 16x16 for PSMT8 - palette16, - csa * 32); + // Use CSA * 32 to offset the VRAM read + _gsMemory.ReadTexPSMCT16(cbp, 1, 0, 0, 16, 16, palette16_T8, csa * 32); Console.WriteLine("Warning: CSA > 0 not properly supported for PSMCT16 yet"); - PSMCT16To32(palette, palette16); + // Convert directly + PSMCT16To32(palette, palette16_T8); break; default: @@ -214,10 +210,14 @@ protected Image GetImageData(PGLUtexture texture, TextureClutPatch textu { for (var x = 0; x < fullWidth; x++) { - img[x, y] = pixels[y * fullWidth + x]; - - byte a = texture.tex0.PSM == SCE_GS_PSM.SCE_GS_PSMCT24 ? (byte)0xFF : (byte)Tex1Utils.Normalize(img[x, y].A, 0x00, 0x80, 0x00, 0xFF); - img[x, y] = new Rgba32(img[x, y].R, img[x, y].G, img[x, y].B, a); // Rescale alpha 0-128 to 0-256. PS2 things + // 1. Grab raw pixel struct + Rgba32 p = pixels[y * fullWidth + x]; + + // 2. Halve the alpha channel + p.A = texture.tex0.PSM == SCE_GS_PSM.SCE_GS_PSMCT24 ? (byte)0xFF : (byte)Tex1Utils.Normalize(p.A, 0x00, 0x80, 0x00, 0xFF); + + // 3. Assign + img[x, y] = p; } } @@ -272,18 +272,23 @@ protected static Rgba32[] MakeTiledPalette(Span pal) return outpal; } - protected static void PSMCT16To32(uint[] palette, ushort[] palette16) - { - // Page 72, GS User's Manual - // PSMCT16 stores the higher 5 bits of each color when converting to PSMCT32 - for (int i = 0; i < 16; i++) - { - byte r = (byte)(((palette16[i] >> 0) & 0b11111) << 3); - byte g = (byte)(((palette16[i] >> 5) & 0b11111) << 3); - byte b = (byte)(((palette16[i] >> 10) & 0b11111) << 3); - byte a = (palette16[i] >> 15 == 1) ? (byte)0x80 : (byte)0x00; - - palette[i] = (uint)(r | g << 8 | b << 16 | a << 24); - } - } -} + protected static void PSMCT16To32(uint[] palette, ushort[] palette16) + { + for (int i = 0; i < palette16.Length; i++) + { + // Extract 5-bit values and correctly expand them to 8-bit (0-255) + byte r = (byte)((palette16[i] >> 0) & 0b11111); + byte g = (byte)((palette16[i] >> 5) & 0b11111); + byte b = (byte)((palette16[i] >> 10) & 0b11111); + + r = (byte)((r << 3) | (r >> 2)); + g = (byte)((g << 3) | (g >> 2)); + b = (byte)((b << 3) | (b >> 2)); + + byte a = ((palette16[i] & 0x8000) != 0) ? (byte)0x80 : (byte)0x00; // Use a direct bitmask against the 15th bit + + // Always write to the exact same index in the image's local palette + palette[i] = (uint)(r | g << 8 | b << 16 | a << 24); + } + } +} \ No newline at end of file