From 2522bef3a90d4d56404cb8c6caf969a625b5c2ce Mon Sep 17 00:00:00 2001 From: crxhvrd Date: Fri, 1 May 2026 06:11:00 +0300 Subject: [PATCH 1/3] Update CodeWalker with local changes (AwcShaderFile, ShaderDisassembler, and various fixes) --- .../GameFiles/FileTypes/AwcShaderFile.cs | 575 ++++++++++++++++++ CodeWalker/CodeWalker.csproj | 9 + CodeWalker/ExploreForm.cs | 11 + CodeWalker/Forms/FxcForm.Designer.cs | 196 +++++- CodeWalker/Forms/FxcForm.cs | 364 ++++++++++- CodeWalker/Forms/ShaderDisassembler.cs | 152 +++++ 6 files changed, 1264 insertions(+), 43 deletions(-) create mode 100644 CodeWalker.Core/GameFiles/FileTypes/AwcShaderFile.cs create mode 100644 CodeWalker/Forms/ShaderDisassembler.cs diff --git a/CodeWalker.Core/GameFiles/FileTypes/AwcShaderFile.cs b/CodeWalker.Core/GameFiles/FileTypes/AwcShaderFile.cs new file mode 100644 index 000000000..75d4123dc --- /dev/null +++ b/CodeWalker.Core/GameFiles/FileTypes/AwcShaderFile.cs @@ -0,0 +1,575 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Text; +using TC = System.ComponentModel.TypeConverterAttribute; +using EXP = System.ComponentModel.ExpandableObjectConverter; + +// AWC Shader Library (SGD2 / Shader Group Data v2) reader & writer. +// Used by GTA V Enhanced (Gen9) compiled-shader containers. Distinct from the +// audio Audio Wave Container (AwcFile.cs / magic ADAT) which shares the .awc +// extension. Ported from the Python reference parser at +// GTATOOLS/fxc/shadermanager/awclib (parser.py, models.py, awc_writer.py). + +namespace CodeWalker.GameFiles +{ + public enum AwcShaderValueType : ushort + { + Bool = 0, + Uint = 1, + Uint2 = 2, + Uint3 = 3, + Uint4 = 4, + Int = 5, + Int2 = 6, + Int3 = 7, + Int4 = 8, + Float = 9, + Float2 = 10, + Float3 = 11, + Float4 = 12, + Float4x3 = 13, + Float4x4 = 14, + } + + public enum AwcShaderResourceType : ushort + { + Texture2D = 0x0102, + Texture2DArray = 0x0142, + TextureCube = 0x0202, + Texture3D = 0x0302, + Buffer = 0x0401, + StructuredBuffer = 0x0405, + ByteAddressBuffer = 0x0407, + RWTexture2D = 0x011C, + RWTexture2DArray = 0x015C, + RWStructuredBufferAppend = 0x040E, + RWStructuredBuffer = 0x0414, + RWStructuredBufferConsume= 0x0416, + RWByteAddressBuffer = 0x0418, + SamplerState = 0x0423, + ConstantBuffer = 0x0430, + } + + public enum AwcShaderStage + { + Vertex, + Pixel, + Geometry, + Domain, + Hull, + Compute, + } + + [TC(typeof(EXP))] + public class AwcShaderCBufferData + { + public AwcShaderValueType Type { get; set; } + public ushort ArraySize { get; set; } + public ushort PackOffset { get; set; } + public uint NameOffset { get; set; } + public string Name { get; set; } + [Browsable(false)] public byte[] NameHashData { get; set; } = new byte[14]; + + public string TypeName => Type.ToString(); + + public override string ToString() => Name + " : " + Type + (ArraySize > 1 ? "[" + ArraySize + "]" : string.Empty); + } + + [TC(typeof(EXP))] + public class AwcShaderRegister + { + public AwcShaderResourceType ResourceType { get; set; } + public ushort RegisterSlot { get; set; } + public byte CBufferCount { get; set; } + public byte NumDescriptors { get; set; } + public byte RegisterSpace { get; set; } + public byte Reserved { get; set; } + public ushort CBufferDataOffset { get; set; } + public ushort RegStringOffset { get; set; } + public string Name { get; set; } + [Browsable(false)] public byte[] ExtraData { get; set; } = new byte[16]; + public AwcShaderCBufferData[] CBuffers { get; set; } + + public string RegisterPrefix + { + get + { + switch (ResourceType) + { + case AwcShaderResourceType.ConstantBuffer: return "b"; + case AwcShaderResourceType.SamplerState: return "s"; + case AwcShaderResourceType.Texture2D: + case AwcShaderResourceType.Texture2DArray: + case AwcShaderResourceType.TextureCube: + case AwcShaderResourceType.Texture3D: + case AwcShaderResourceType.Buffer: + case AwcShaderResourceType.StructuredBuffer: + case AwcShaderResourceType.ByteAddressBuffer: + return "t"; + default: return "u"; + } + } + } + + public string Slot => RegisterPrefix + RegisterSlot + (RegisterSpace != 0 ? ",space" + RegisterSpace : string.Empty); + + public override string ToString() => Slot + " " + Name + " (" + ResourceType + ")"; + } + + [TC(typeof(EXP))] + public class AwcShader + { + public string Name { get; set; } + public byte WaveSize { get; set; } + public uint Size { get; set; } + [Browsable(false)] public byte[] Binary { get; set; } + public ulong Hash { get; set; } + [Browsable(false)] public byte[] RootSigData { get; set; } + public uint BlockSize { get; set; } + public ushort RegCount { get; set; } + public ushort CBufferCount { get; set; } + public ushort TexCount { get; set; } + public ushort BlockSizeCopy { get; set; } + public AwcShaderRegister[] Registers { get; set; } + public AwcShaderStage Stage { get; set; } + + // Original-on-disk fragments preserved so unchanged shaders round-trip + // byte-for-byte. Stale once the user mutates Binary / Size / Registers. + [Browsable(false)] public byte NameLengthByte { get; set; } + [Browsable(false)] public byte[] NameBytes { get; set; } + [Browsable(false)] public byte[] OriginalBlockData { get; set; } + [Browsable(false)] public bool BinaryDirty { get; set; } + [Browsable(false)] public bool MetadataDirty { get; set; } + + public string StageName + { + get + { + switch (Stage) + { + case AwcShaderStage.Vertex: return "VS"; + case AwcShaderStage.Pixel: return "PS"; + case AwcShaderStage.Geometry: return "GS"; + case AwcShaderStage.Domain: return "DS"; + case AwcShaderStage.Hull: return "HS"; + case AwcShaderStage.Compute: return "CS"; + default: return "?"; + } + } + } + + public string HashHex => "0x" + Hash.ToString("X16"); + + public override string ToString() => StageName + " " + Name; + } + + [TC(typeof(EXP))] + public class AwcShaderFile : PackedFile + { + public const uint MagicSGD2 = 0x32444753; // "SGD2" + + public string Name { get; set; } + public RpfFileEntry FileEntry { get; set; } + public string Magic { get; set; } + public AwcShader[] VertexShaders { get; set; } + public AwcShader[] PixelShaders { get; set; } + public AwcShader[] GeometryShaders { get; set; } + public AwcShader[] DomainShaders { get; set; } + public AwcShader[] HullShaders { get; set; } + public AwcShader[] ComputeShaders { get; set; } + + [Browsable(false)] public byte[] FooterData { get; set; } + + public int VertexCount => VertexShaders?.Length ?? 0; + public int PixelCount => PixelShaders?.Length ?? 0; + public int GeometryCount => GeometryShaders?.Length ?? 0; + public int DomainCount => DomainShaders?.Length ?? 0; + public int HullCount => HullShaders?.Length ?? 0; + public int ComputeCount => ComputeShaders?.Length ?? 0; + public int TotalShaderCount => VertexCount + PixelCount + GeometryCount + DomainCount + HullCount + ComputeCount; + + public IEnumerable AllShaders() + { + if (VertexShaders != null) foreach (var s in VertexShaders) yield return s; + if (PixelShaders != null) foreach (var s in PixelShaders) yield return s; + if (GeometryShaders != null) foreach (var s in GeometryShaders) yield return s; + if (DomainShaders != null) foreach (var s in DomainShaders) yield return s; + if (HullShaders != null) foreach (var s in HullShaders) yield return s; + if (ComputeShaders != null) foreach (var s in ComputeShaders) yield return s; + } + + public void Load(byte[] data, RpfFileEntry entry) + { + FileEntry = entry; + Name = entry?.Name; + + if (data == null || data.Length < 4) + throw new InvalidDataException("AWC Shader Library: empty data."); + + if (BitConverter.ToUInt32(data, 0) != MagicSGD2) + throw new InvalidDataException("AWC Shader Library: not an SGD2 file."); + + using (var ms = new MemoryStream(data)) + using (var br = new BinaryReader(ms, Encoding.GetEncoding("ISO-8859-1"))) + { + Magic = Encoding.ASCII.GetString(br.ReadBytes(4)); + + VertexShaders = ReadShaderArray(br, AwcShaderStage.Vertex); + PixelShaders = ReadShaderArray(br, AwcShaderStage.Pixel); + GeometryShaders = ReadShaderArray(br, AwcShaderStage.Geometry); + DomainShaders = ReadShaderArray(br, AwcShaderStage.Domain); + HullShaders = ReadShaderArray(br, AwcShaderStage.Hull); + ComputeShaders = ReadShaderArray(br, AwcShaderStage.Compute); + + long remaining = ms.Length - ms.Position; + FooterData = remaining > 0 ? br.ReadBytes((int)remaining) : Array.Empty(); + } + } + + private static AwcShader[] ReadShaderArray(BinaryReader br, AwcShaderStage stage) + { + uint count = br.ReadUInt32(); + var arr = new AwcShader[count]; + for (uint i = 0; i < count; i++) + arr[i] = ReadShader(br, stage); + return arr; + } + + private static AwcShader ReadShader(BinaryReader br, AwcShaderStage stage) + { + byte slen = br.ReadByte(); + byte[] nameBytes = br.ReadBytes(slen); + string name = Encoding.GetEncoding("ISO-8859-1").GetString(nameBytes).TrimEnd('\0'); + + byte wave = br.ReadByte(); + uint size = br.ReadUInt32(); + byte[] binary = br.ReadBytes((int)size); + ulong hash = br.ReadUInt64(); + byte[] rootSig = br.ReadBytes(144); + uint blockSize = br.ReadUInt32(); + + long blockStart = br.BaseStream.Position; + byte[] blockData = br.ReadBytes((int)blockSize); + + // Re-parse the metadata block from blockData so the whole shader + // stays self-contained (no further seeks into the outer stream). + var (regCount, cbCount, texCount, blockSizeCopy, registers) = ParseBlock(blockData); + + return new AwcShader + { + Name = name, + NameLengthByte = slen, + NameBytes = nameBytes, + WaveSize = wave, + Size = size, + Binary = binary, + Hash = hash, + RootSigData = rootSig, + BlockSize = blockSize, + BlockSizeCopy = blockSizeCopy, + RegCount = regCount, + CBufferCount = cbCount, + TexCount = texCount, + Registers = registers, + OriginalBlockData = blockData, + Stage = stage, + }; + } + + private static (ushort reg, ushort cb, ushort tex, ushort blkSizeCopy, AwcShaderRegister[] regs) ParseBlock(byte[] block) + { + using (var ms = new MemoryStream(block)) + using (var br = new BinaryReader(ms)) + { + ushort regCount = br.ReadUInt16(); + ushort cbCount = br.ReadUInt16(); + ushort texCount = br.ReadUInt16(); + ushort blkCopy = br.ReadUInt16(); + + var regs = new AwcShaderRegister[regCount]; + for (int i = 0; i < regCount; i++) + regs[i] = ParseRegister(ms, br); + + return (regCount, cbCount, texCount, blkCopy, regs); + } + } + + private static AwcShaderRegister ParseRegister(MemoryStream ms, BinaryReader br) + { + long headerStart = ms.Position; + + ushort resType = br.ReadUInt16(); + ushort regSlot = br.ReadUInt16(); + byte cbCount = br.ReadByte(); + byte numDesc = br.ReadByte(); + byte regSpace = br.ReadByte(); + byte reserved = br.ReadByte(); + ushort cbDataOffset = br.ReadUInt16(); + ushort regStringOff = br.ReadUInt16(); + + long afterHeader = ms.Position; // headerStart + 12 + byte[] extra = br.ReadBytes(16); + + // Offsets in the binary are relative to headerStart (the parser in + // Python expresses this as (afterHeader + offset - 12)). + string regName; + long savedPos = ms.Position; + ms.Position = headerStart + regStringOff; + regName = ReadCString(br); + ms.Position = savedPos; + + int validCb = cbDataOffset != 0 ? cbCount : 0; + AwcShaderCBufferData[] cbs; + if (validCb > 0) + { + cbs = new AwcShaderCBufferData[validCb]; + ms.Position = headerStart + cbDataOffset; + for (int i = 0; i < validCb; i++) + cbs[i] = ParseCBufferData(ms, br); + } + else + { + cbs = Array.Empty(); + } + + // Move past the 16-byte extra data area, ready for the next register. + ms.Position = afterHeader + 16; + + return new AwcShaderRegister + { + ResourceType = (AwcShaderResourceType)resType, + RegisterSlot = regSlot, + CBufferCount = cbCount, + NumDescriptors = numDesc, + RegisterSpace = regSpace, + Reserved = reserved, + CBufferDataOffset = cbDataOffset, + RegStringOffset = regStringOff, + ExtraData = extra, + Name = regName, + CBuffers = cbs, + }; + } + + private static AwcShaderCBufferData ParseCBufferData(MemoryStream ms, BinaryReader br) + { + long start = ms.Position; + ushort type = br.ReadUInt16(); + ushort arraySize = br.ReadUInt16(); + ushort packOffset = br.ReadUInt16(); + uint nameOffset = br.ReadUInt32(); + + string cbName; + long savedPos = ms.Position; + ms.Position = start + nameOffset; + cbName = ReadCString(br); + ms.Position = savedPos; + + byte[] nameHashData = br.ReadBytes(14); + + return new AwcShaderCBufferData + { + Type = (AwcShaderValueType)type, + ArraySize = arraySize, + PackOffset = packOffset, + NameOffset = nameOffset, + Name = cbName, + NameHashData = nameHashData, + }; + } + + private static string ReadCString(BinaryReader br) + { + var sb = new StringBuilder(); + while (br.BaseStream.Position < br.BaseStream.Length) + { + byte b = br.ReadByte(); + if (b == 0) break; + sb.Append((char)b); + } + return sb.ToString(); + } + + // ---------- Save ---------- + + public byte[] Save() + { + using (var ms = new MemoryStream()) + using (var bw = new BinaryWriter(ms)) + { + bw.Write(MagicSGD2); + + WriteShaderArray(bw, VertexShaders); + WriteShaderArray(bw, PixelShaders); + WriteShaderArray(bw, GeometryShaders); + WriteShaderArray(bw, DomainShaders); + WriteShaderArray(bw, HullShaders); + WriteShaderArray(bw, ComputeShaders); + + if (FooterData != null && FooterData.Length > 0) + bw.Write(FooterData); + + return ms.ToArray(); + } + } + + private static void WriteShaderArray(BinaryWriter bw, AwcShader[] arr) + { + uint count = (uint)(arr?.Length ?? 0); + bw.Write(count); + if (arr == null) return; + for (int i = 0; i < arr.Length; i++) + WriteShader(bw, arr[i]); + } + + private static void WriteShader(BinaryWriter bw, AwcShader s) + { + // Preserve original name bytes/length (avoids Latin-1 round-trip risk). + if (s.NameBytes != null) + { + bw.Write(s.NameLengthByte); + bw.Write(s.NameBytes); + } + else + { + byte[] nb = Encoding.GetEncoding("ISO-8859-1").GetBytes(s.Name ?? string.Empty); + byte[] nbz = new byte[nb.Length + 1]; + Array.Copy(nb, nbz, nb.Length); + bw.Write((byte)nbz.Length); + bw.Write(nbz); + } + + bw.Write(s.WaveSize); + uint binSize = (uint)(s.Binary?.Length ?? 0); + bw.Write(binSize); + if (binSize > 0) bw.Write(s.Binary); + + bw.Write(s.Hash); + bw.Write(s.RootSigData ?? new byte[144]); + + byte[] block; + if (s.MetadataDirty) + { + block = BuildMetadataBlock(s); + } + else if (s.OriginalBlockData != null) + { + block = s.OriginalBlockData; + } + else + { + block = BuildMetadataBlock(s); + } + + bw.Write((uint)block.Length); + bw.Write(block); + } + + // Mirrors awc_writer.py:_build_metadata_block. Unused in Phase 1 (binary- + // only imports keep MetadataDirty=false), but kept so the data model is + // round-trippable end-to-end. + private static byte[] BuildMetadataBlock(AwcShader s) + { + var regs = s.Registers ?? Array.Empty(); + ushort regCount = (ushort)regs.Length; + ushort cbCountTotal = 0; + for (int i = 0; i < regs.Length; i++) + cbCountTotal += (ushort)(regs[i].CBuffers?.Length ?? 0); + + var buf = new List(256); + + void WriteU16(ushort v) { buf.Add((byte)(v & 0xFF)); buf.Add((byte)(v >> 8)); } + void WriteU32At(int pos, uint v) { buf[pos] = (byte)(v & 0xFF); buf[pos + 1] = (byte)((v >> 8) & 0xFF); buf[pos + 2] = (byte)((v >> 16) & 0xFF); buf[pos + 3] = (byte)((v >> 24) & 0xFF); } + void WriteU16At(int pos, ushort v) { buf[pos] = (byte)(v & 0xFF); buf[pos + 1] = (byte)(v >> 8); } + + WriteU16(regCount); + WriteU16(cbCountTotal); + WriteU16(s.TexCount); + WriteU16(0); // block_size_copy placeholder + + const int headerSize = 8; + int regHeadersStart = headerSize; + int regHeadersSize = regCount * 28; + for (int i = 0; i < regHeadersSize; i++) buf.Add(0); + + int[] cbStructPos = new int[regCount]; + for (int i = 0; i < regs.Length; i++) + { + var cbs = regs[i].CBuffers; + if (cbs != null && cbs.Length > 0) + { + cbStructPos[i] = buf.Count; + for (int k = 0; k < cbs.Length * 24; k++) buf.Add(0); + } + else cbStructPos[i] = 0; + } + + int[] regStringPos = new int[regCount]; + for (int i = 0; i < regs.Length; i++) + { + regStringPos[i] = buf.Count; + byte[] nb = Encoding.GetEncoding("ISO-8859-1").GetBytes(regs[i].Name ?? string.Empty); + buf.AddRange(nb); + buf.Add(0); + } + + for (int i = 0; i < regs.Length; i++) + { + var cbs = regs[i].CBuffers; + if (cbs == null || cbs.Length == 0) continue; + int cbBase = cbStructPos[i]; + for (int j = 0; j < cbs.Length; j++) + { + var cb = cbs[j]; + int pCb = cbBase + j * 24; + + int cbStrPos = buf.Count; + byte[] nb = Encoding.GetEncoding("ISO-8859-1").GetBytes(cb.Name ?? string.Empty); + buf.AddRange(nb); + buf.Add(0); + + uint cbNameOffset = (uint)(cbStrPos - pCb); + WriteU16At(pCb + 0, (ushort)cb.Type); + WriteU16At(pCb + 2, cb.ArraySize); + WriteU16At(pCb + 4, cb.PackOffset); + WriteU32At(pCb + 6, cbNameOffset); + + byte[] hashBytes = cb.NameHashData; + if (hashBytes == null || hashBytes.Length != 14) hashBytes = new byte[14]; + for (int k = 0; k < 14; k++) buf[pCb + 10 + k] = hashBytes[k]; + } + } + + for (int i = 0; i < regs.Length; i++) + { + int pReg = regHeadersStart + i * 28; + var r = regs[i]; + int cbCount = r.CBuffers?.Length ?? 0; + ushort cbDataOffset = (ushort)(cbCount > 0 ? (cbStructPos[i] - pReg) : 0); + ushort regStrOffset = (ushort)(regStringPos[i] - pReg); + + WriteU16At(pReg + 0, (ushort)r.ResourceType); + WriteU16At(pReg + 2, r.RegisterSlot); + buf[pReg + 4] = (byte)cbCount; + buf[pReg + 5] = r.NumDescriptors; + buf[pReg + 6] = r.RegisterSpace; + buf[pReg + 7] = r.Reserved; + WriteU16At(pReg + 8, cbDataOffset); + WriteU16At(pReg + 10, regStrOffset); + + byte[] extra = r.ExtraData; + if (extra == null || extra.Length != 16) extra = new byte[16]; + for (int k = 0; k < 16; k++) buf[pReg + 12 + k] = extra[k]; + } + + if ((buf.Count & 1) != 0) buf.Add(0); + + ushort total = (ushort)buf.Count; + WriteU16At(6, total); + + return buf.ToArray(); + } + } +} diff --git a/CodeWalker/CodeWalker.csproj b/CodeWalker/CodeWalker.csproj index c2cba3a51..e82cd959a 100644 --- a/CodeWalker/CodeWalker.csproj +++ b/CodeWalker/CodeWalker.csproj @@ -51,4 +51,13 @@ + + + PreserveNewest + + + PreserveNewest + + + \ No newline at end of file diff --git a/CodeWalker/ExploreForm.cs b/CodeWalker/ExploreForm.cs index 593ec4e97..2efcc68f0 100644 --- a/CodeWalker/ExploreForm.cs +++ b/CodeWalker/ExploreForm.cs @@ -1855,6 +1855,17 @@ private void ViewCut(string name, string path, byte[] data, RpfFileEntry e) } private void ViewAwc(string name, string path, byte[] data, RpfFileEntry e) { + // .awc is overloaded: audio (ADAT/TADA) vs shader library (SGD2). + // Sniff the magic so SGD2 shader libraries open in the FxcForm. + if (data != null && data.Length >= 4 && BitConverter.ToUInt32(data, 0) == AwcShaderFile.MagicSGD2) + { + var awcsh = RpfFile.GetFile(e, data); + FxcForm fx = new FxcForm(); + fx.Show(); + fx.LoadAwcShader(awcsh, e, this); + return; + } + var awc = RpfFile.GetFile(e, data); AwcForm f = new AwcForm(); f.Show(); diff --git a/CodeWalker/Forms/FxcForm.Designer.cs b/CodeWalker/Forms/FxcForm.Designer.cs index 225b6cb0e..d3a91bf50 100644 --- a/CodeWalker/Forms/FxcForm.Designer.cs +++ b/CodeWalker/Forms/FxcForm.Designer.cs @@ -35,9 +35,23 @@ private void InitializeComponent() this.DetailsTabPage = new System.Windows.Forms.TabPage(); this.DetailsPropertyGrid = new CodeWalker.WinForms.PropertyGridFix(); this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.ShadersListPanel = new System.Windows.Forms.Panel(); + this.SearchPanel = new System.Windows.Forms.Panel(); + this.SearchTextBox = new System.Windows.Forms.TextBox(); + this.TypeFilterComboBox = new System.Windows.Forms.ComboBox(); + this.SearchLabel = new System.Windows.Forms.Label(); this.ShaderPanel = new System.Windows.Forms.Panel(); this.ShadersListView = new System.Windows.Forms.ListView(); this.ShadersNameColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.ShadersTypeColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); + this.MainMenu = new System.Windows.Forms.MenuStrip(); + this.FileMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SaveMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SaveAsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.ExportAllMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.ShaderContextMenu = new System.Windows.Forms.ContextMenuStrip(this.components); + this.ExportCsoMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.ImportCsoMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ShaderTextBox = new FastColoredTextBoxNS.FastColoredTextBox(); this.statusStrip1 = new System.Windows.Forms.StatusStrip(); this.StatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); @@ -54,6 +68,10 @@ private void InitializeComponent() this.splitContainer1.Panel1.SuspendLayout(); this.splitContainer1.Panel2.SuspendLayout(); this.splitContainer1.SuspendLayout(); + this.ShadersListPanel.SuspendLayout(); + this.SearchPanel.SuspendLayout(); + this.MainMenu.SuspendLayout(); + this.ShaderContextMenu.SuspendLayout(); this.ShaderPanel.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.ShaderTextBox)).BeginInit(); this.statusStrip1.SuspendLayout(); @@ -74,10 +92,10 @@ private void InitializeComponent() this.MainTabControl.Controls.Add(this.ShadersTabPage); this.MainTabControl.Controls.Add(this.TechniquesTabPage); this.MainTabControl.Controls.Add(this.DetailsTabPage); - this.MainTabControl.Location = new System.Drawing.Point(2, 3); + this.MainTabControl.Location = new System.Drawing.Point(2, 27); this.MainTabControl.Name = "MainTabControl"; this.MainTabControl.SelectedIndex = 0; - this.MainTabControl.Size = new System.Drawing.Size(784, 480); + this.MainTabControl.Size = new System.Drawing.Size(784, 456); this.MainTabControl.TabIndex = 0; // // ShadersTabPage @@ -119,9 +137,69 @@ private void InitializeComponent() this.splitContainer1.Name = "splitContainer1"; // // splitContainer1.Panel1 - // - this.splitContainer1.Panel1.Controls.Add(this.ShadersListView); - // + // + this.splitContainer1.Panel1.Controls.Add(this.ShadersListPanel); + // + // ShadersListPanel + // + this.ShadersListPanel.Controls.Add(this.ShadersListView); + this.ShadersListPanel.Controls.Add(this.SearchPanel); + this.ShadersListPanel.Dock = System.Windows.Forms.DockStyle.Fill; + this.ShadersListPanel.Location = new System.Drawing.Point(0, 0); + this.ShadersListPanel.Name = "ShadersListPanel"; + this.ShadersListPanel.Size = new System.Drawing.Size(235, 448); + this.ShadersListPanel.TabIndex = 0; + // + // SearchPanel + // + this.SearchPanel.Controls.Add(this.SearchTextBox); + this.SearchPanel.Controls.Add(this.TypeFilterComboBox); + this.SearchPanel.Controls.Add(this.SearchLabel); + this.SearchPanel.Dock = System.Windows.Forms.DockStyle.Top; + this.SearchPanel.Location = new System.Drawing.Point(0, 0); + this.SearchPanel.Name = "SearchPanel"; + this.SearchPanel.Size = new System.Drawing.Size(235, 56); + this.SearchPanel.TabIndex = 0; + // + // SearchLabel + // + this.SearchLabel.AutoSize = true; + this.SearchLabel.Location = new System.Drawing.Point(3, 7); + this.SearchLabel.Name = "SearchLabel"; + this.SearchLabel.Size = new System.Drawing.Size(44, 13); + this.SearchLabel.TabIndex = 0; + this.SearchLabel.Text = "Search:"; + // + // SearchTextBox + // + this.SearchTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.SearchTextBox.Location = new System.Drawing.Point(53, 4); + this.SearchTextBox.Name = "SearchTextBox"; + this.SearchTextBox.Size = new System.Drawing.Size(178, 20); + this.SearchTextBox.TabIndex = 1; + this.SearchTextBox.TextChanged += new System.EventHandler(this.SearchTextBox_TextChanged); + // + // TypeFilterComboBox + // + this.TypeFilterComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.TypeFilterComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.TypeFilterComboBox.FormattingEnabled = true; + this.TypeFilterComboBox.Items.AddRange(new object[] { + "All", + "Vertex", + "Pixel", + "Geometry", + "Domain", + "Hull", + "Compute"}); + this.TypeFilterComboBox.Location = new System.Drawing.Point(53, 30); + this.TypeFilterComboBox.Name = "TypeFilterComboBox"; + this.TypeFilterComboBox.Size = new System.Drawing.Size(178, 21); + this.TypeFilterComboBox.TabIndex = 2; + this.TypeFilterComboBox.SelectedIndexChanged += new System.EventHandler(this.TypeFilterComboBox_SelectedIndexChanged); + // // splitContainer1.Panel2 // this.splitContainer1.Panel2.Controls.Add(this.ShaderPanel); @@ -140,25 +218,97 @@ private void InitializeComponent() this.ShaderPanel.TabIndex = 0; // // ShadersListView - // + // this.ShadersListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { + this.ShadersTypeColumn, this.ShadersNameColumn}); this.ShadersListView.Dock = System.Windows.Forms.DockStyle.Fill; this.ShadersListView.FullRowSelect = true; this.ShadersListView.HideSelection = false; - this.ShadersListView.Location = new System.Drawing.Point(0, 0); + this.ShadersListView.Location = new System.Drawing.Point(0, 56); this.ShadersListView.MultiSelect = false; this.ShadersListView.Name = "ShadersListView"; - this.ShadersListView.Size = new System.Drawing.Size(235, 448); - this.ShadersListView.TabIndex = 0; + this.ShadersListView.Size = new System.Drawing.Size(235, 392); + this.ShadersListView.TabIndex = 1; this.ShadersListView.UseCompatibleStateImageBehavior = false; this.ShadersListView.View = System.Windows.Forms.View.Details; + this.ShadersListView.ContextMenuStrip = this.ShaderContextMenu; this.ShadersListView.SelectedIndexChanged += new System.EventHandler(this.ShadersListView_SelectedIndexChanged); - // + // + // ShadersTypeColumn + // + this.ShadersTypeColumn.Text = "Type"; + this.ShadersTypeColumn.Width = 40; + // // ShadersNameColumn - // + // this.ShadersNameColumn.Text = "Name"; - this.ShadersNameColumn.Width = 208; + this.ShadersNameColumn.Width = 168; + // + // MainMenu + // + this.MainMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.FileMenuItem}); + this.MainMenu.Location = new System.Drawing.Point(0, 0); + this.MainMenu.Name = "MainMenu"; + this.MainMenu.Size = new System.Drawing.Size(788, 24); + this.MainMenu.TabIndex = 2; + this.MainMenu.Text = "MainMenu"; + // + // FileMenuItem + // + this.FileMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.SaveMenuItem, + this.SaveAsMenuItem, + this.ExportAllMenuItem}); + this.FileMenuItem.Name = "FileMenuItem"; + this.FileMenuItem.Size = new System.Drawing.Size(37, 20); + this.FileMenuItem.Text = "File"; + // + // SaveMenuItem + // + this.SaveMenuItem.Name = "SaveMenuItem"; + this.SaveMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S))); + this.SaveMenuItem.Size = new System.Drawing.Size(186, 22); + this.SaveMenuItem.Text = "Save"; + this.SaveMenuItem.Click += new System.EventHandler(this.SaveMenuItem_Click); + // + // SaveAsMenuItem + // + this.SaveAsMenuItem.Name = "SaveAsMenuItem"; + this.SaveAsMenuItem.Size = new System.Drawing.Size(186, 22); + this.SaveAsMenuItem.Text = "Save As..."; + this.SaveAsMenuItem.Click += new System.EventHandler(this.SaveAsMenuItem_Click); + // + // ExportAllMenuItem + // + this.ExportAllMenuItem.Name = "ExportAllMenuItem"; + this.ExportAllMenuItem.Size = new System.Drawing.Size(186, 22); + this.ExportAllMenuItem.Text = "Export All Shaders..."; + this.ExportAllMenuItem.Click += new System.EventHandler(this.ExportAllMenuItem_Click); + // + // ShaderContextMenu + // + this.ShaderContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.ExportCsoMenuItem, + this.ImportCsoMenuItem}); + this.ShaderContextMenu.Name = "ShaderContextMenu"; + this.ShaderContextMenu.Size = new System.Drawing.Size(190, 48); + this.ShaderContextMenu.Opening += new System.ComponentModel.CancelEventHandler(this.ShaderContextMenu_Opening); + // + // ExportCsoMenuItem + // + this.ExportCsoMenuItem.Name = "ExportCsoMenuItem"; + this.ExportCsoMenuItem.Size = new System.Drawing.Size(189, 22); + this.ExportCsoMenuItem.Text = "Export CSO..."; + this.ExportCsoMenuItem.Click += new System.EventHandler(this.ExportCsoMenuItem_Click); + // + // ImportCsoMenuItem + // + this.ImportCsoMenuItem.Name = "ImportCsoMenuItem"; + this.ImportCsoMenuItem.Size = new System.Drawing.Size(189, 22); + this.ImportCsoMenuItem.Text = "Import CSO (Replace)..."; + this.ImportCsoMenuItem.Click += new System.EventHandler(this.ImportCsoMenuItem_Click); // // ShaderTextBox // @@ -330,6 +480,8 @@ private void InitializeComponent() this.ClientSize = new System.Drawing.Size(788, 508); this.Controls.Add(this.statusStrip1); this.Controls.Add(this.MainTabControl); + this.Controls.Add(this.MainMenu); + this.MainMenuStrip = this.MainMenu; this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.Name = "FxcForm"; this.Text = "FXC Viewer - CodeWalker by dexyfex"; @@ -340,6 +492,12 @@ private void InitializeComponent() this.splitContainer1.Panel2.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); this.splitContainer1.ResumeLayout(false); + this.ShadersListPanel.ResumeLayout(false); + this.SearchPanel.ResumeLayout(false); + this.SearchPanel.PerformLayout(); + this.MainMenu.ResumeLayout(false); + this.MainMenu.PerformLayout(); + this.ShaderContextMenu.ResumeLayout(false); this.ShaderPanel.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)(this.ShaderTextBox)).EndInit(); this.statusStrip1.ResumeLayout(false); @@ -375,5 +533,19 @@ private void InitializeComponent() private System.Windows.Forms.ColumnHeader TechniquesNameColumn; private System.Windows.Forms.Panel TechniquePanel; private FastColoredTextBoxNS.FastColoredTextBox TechniqueTextBox; + private System.Windows.Forms.Panel ShadersListPanel; + private System.Windows.Forms.Panel SearchPanel; + private System.Windows.Forms.TextBox SearchTextBox; + private System.Windows.Forms.ComboBox TypeFilterComboBox; + private System.Windows.Forms.Label SearchLabel; + private System.Windows.Forms.ColumnHeader ShadersTypeColumn; + private System.Windows.Forms.MenuStrip MainMenu; + private System.Windows.Forms.ToolStripMenuItem FileMenuItem; + private System.Windows.Forms.ToolStripMenuItem SaveMenuItem; + private System.Windows.Forms.ToolStripMenuItem SaveAsMenuItem; + private System.Windows.Forms.ToolStripMenuItem ExportCsoMenuItem; + private System.Windows.Forms.ToolStripMenuItem ImportCsoMenuItem; + private System.Windows.Forms.ToolStripMenuItem ExportAllMenuItem; + private System.Windows.Forms.ContextMenuStrip ShaderContextMenu; } } \ No newline at end of file diff --git a/CodeWalker/Forms/FxcForm.cs b/CodeWalker/Forms/FxcForm.cs index 7d1dbfc56..8f35d4af3 100644 --- a/CodeWalker/Forms/FxcForm.cs +++ b/CodeWalker/Forms/FxcForm.cs @@ -1,9 +1,7 @@ -using CodeWalker.GameFiles; +using CodeWalker.GameFiles; using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -14,6 +12,10 @@ namespace CodeWalker.Forms public partial class FxcForm : Form { private FxcFile Fxc; + private AwcShaderFile AwcShader; + private AwcShader SelectedAwcShader; + private RpfFileEntry rpfFileEntry; + private ExploreForm exploreForm; private string fileName; public string FileName @@ -28,25 +30,57 @@ public string FileName public string FilePath { get; set; } - - public FxcForm() { InitializeComponent(); + if (TypeFilterComboBox.Items.Count > 0) + TypeFilterComboBox.SelectedIndex = 0; + UpdateAwcModeUi(awcMode: false); } - - private void UpdateFormTitle() { - Text = fileName + " - FXC Viewer - CodeWalker by dexyfex"; + string suffix = AwcShader != null ? "AWC Shader Library Viewer" : "FXC Viewer"; + Text = fileName + " - " + suffix + " - CodeWalker by dexyfex"; + } + + private void UpdateAwcModeUi(bool awcMode) + { + // Menu items only meaningful in AWC mode + SaveMenuItem.Enabled = awcMode; + SaveAsMenuItem.Enabled = awcMode; + ExportAllMenuItem.Enabled = awcMode; + // Per-shader items live on the right-click context menu and are + // gated on selection — see ShaderContextMenu_Opening. + + // Search/type filter only for AWC (FXC list is small and unsegmented). + // SearchPanel is docked Top; toggling visibility lets the docked + // ShadersListView fill the freed space automatically. + SearchPanel.Visible = awcMode; + + // Hide Type column in FXC mode + ShadersTypeColumn.Width = awcMode ? 40 : 0; + + // Hide Techniques tab in AWC mode (AWC has no techniques) + if (awcMode) + { + if (MainTabControl.TabPages.Contains(TechniquesTabPage)) + MainTabControl.TabPages.Remove(TechniquesTabPage); + } + else + { + if (!MainTabControl.TabPages.Contains(TechniquesTabPage)) + MainTabControl.TabPages.Insert(1, TechniquesTabPage); + } } public void LoadFxc(FxcFile fxc) { Fxc = fxc; + AwcShader = null; + UpdateAwcModeUi(awcMode: false); fileName = fxc?.Name; if (string.IsNullOrEmpty(fileName)) @@ -65,7 +99,8 @@ public void LoadFxc(FxcFile fxc) foreach (var shader in fxc.Shaders) { - var item = ShadersListView.Items.Add(shader.Name); + var item = ShadersListView.Items.Add(string.Empty); // Type col empty in FXC mode + item.SubItems.Add(shader.Name); item.Tag = shader; } @@ -79,7 +114,80 @@ public void LoadFxc(FxcFile fxc) } - StatusLabel.Text = (fxc.Shaders?.Length??0).ToString() + " shaders, " + (fxc.Techniques?.Length??0).ToString() + " techniques"; + StatusLabel.Text = (fxc.Shaders?.Length ?? 0) + " shaders, " + (fxc.Techniques?.Length ?? 0) + " techniques"; + } + + + public void LoadAwcShader(AwcShaderFile awc, RpfFileEntry entry, ExploreForm owner) + { + Fxc = null; + AwcShader = awc; + rpfFileEntry = entry; + exploreForm = owner; + UpdateAwcModeUi(awcMode: true); + + fileName = entry?.Name ?? awc?.Name; + UpdateFormTitle(); + + DetailsPropertyGrid.SelectedObject = awc; + + RebuildShadersList(); + + StatusLabel.Text = BuildAwcStatus(); + } + + private string BuildAwcStatus() + { + if (AwcShader == null) return "Ready"; + return AwcShader.TotalShaderCount + " shaders (" + + AwcShader.VertexCount + " VS, " + + AwcShader.PixelCount + " PS, " + + AwcShader.GeometryCount + " GS, " + + AwcShader.DomainCount + " DS, " + + AwcShader.HullCount + " HS, " + + AwcShader.ComputeCount + " CS)"; + } + + private void RebuildShadersList() + { + ShadersListView.BeginUpdate(); + try + { + ShadersListView.Items.Clear(); + if (AwcShader == null) return; + + string filter = SearchTextBox.Text?.Trim(); + bool hasFilter = !string.IsNullOrEmpty(filter); + string typeFilter = (TypeFilterComboBox.SelectedItem as string) ?? "All"; + + foreach (var s in AwcShader.AllShaders()) + { + if (typeFilter != "All" && !MatchesStage(s.Stage, typeFilter)) continue; + if (hasFilter && (s.Name == null || s.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)) continue; + + var item = ShadersListView.Items.Add(s.StageName); + item.SubItems.Add(s.Name); + item.Tag = s; + } + } + finally + { + ShadersListView.EndUpdate(); + } + } + + private static bool MatchesStage(AwcShaderStage stage, string label) + { + switch (label) + { + case "Vertex": return stage == AwcShaderStage.Vertex; + case "Pixel": return stage == AwcShaderStage.Pixel; + case "Geometry": return stage == AwcShaderStage.Geometry; + case "Domain": return stage == AwcShaderStage.Domain; + case "Hull": return stage == AwcShaderStage.Hull; + case "Compute": return stage == AwcShaderStage.Compute; + default: return true; + } } @@ -110,6 +218,71 @@ private void LoadShader(FxcShader s) } } + private void LoadAwcShader(AwcShader s) + { + SelectedAwcShader = s; + DetailsPropertyGrid.SelectedObject = (object)s ?? AwcShader; + + if (s == null) + { + ShaderPanel.Enabled = false; + ShaderTextBox.Text = string.Empty; + return; + } + ShaderPanel.Enabled = true; + + var header = BuildShaderHeader(s); + ShaderTextBox.Text = header + "\r\n// Disassembling... (dxc -dumpbin)\r\n"; + + // dxc/fxc takes 50-500ms — keep the UI responsive. + var binary = s.Binary; + var name = s.Name; + var stage = s.StageName; + Task.Run(() => + { + string asm = ShaderDisassembler.Disassemble(binary, name, out string err); + string body = !string.IsNullOrEmpty(asm) + ? asm + : "// Disassembly unavailable.\r\n// " + (err ?? "Unknown error").Replace("\n", "\n// "); + BeginInvoke((Action)(() => + { + if (SelectedAwcShader == null || !ReferenceEquals(SelectedAwcShader.Binary, binary)) return; + ShaderTextBox.Text = header + "\r\n" + body; + })); + }); + } + + private static string BuildShaderHeader(AwcShader s) + { + var sb = new StringBuilder(); + sb.AppendLine("// " + s.StageName + " " + s.Name); + sb.AppendLine("// Hash: " + s.HashHex); + sb.AppendLine("// Wave: " + s.WaveSize); + sb.AppendLine("// Size: " + s.Size + " bytes"); + sb.AppendLine("// Block: " + s.BlockSize + " bytes"); + sb.AppendLine("// Counts: reg=" + s.RegCount + " cb=" + s.CBufferCount + " tex=" + s.TexCount); + + if (s.Registers != null && s.Registers.Length > 0) + { + sb.AppendLine("//"); + sb.AppendLine("// Registers:"); + foreach (var r in s.Registers) + { + sb.Append("// ").Append(r.Slot.PadRight(10)).Append(' ').Append((r.Name ?? string.Empty).PadRight(32)).Append(" (").Append(r.ResourceType).AppendLine(")"); + if (r.CBuffers != null && r.CBuffers.Length > 0) + { + foreach (var cb in r.CBuffers) + { + sb.Append("// +0x").Append(cb.PackOffset.ToString("X4")).Append(" ") + .Append(cb.Type).Append(cb.ArraySize > 1 ? "[" + cb.ArraySize + "]" : string.Empty) + .Append(" ").AppendLine(cb.Name); + } + } + } + } + return sb.ToString(); + } + private void LoadTechnique(FxcTechnique t) { if (t == null) @@ -128,7 +301,7 @@ private void LoadTechnique(FxcTechnique t) for (int i = 0; i < t.Passes.Length; i++) { var pass = t.Passes[i]; - sb.AppendLine(" pass p" + i.ToString());// + pass.ToString()); + sb.AppendLine(" pass p" + i.ToString()); sb.AppendLine(" {"); var vs = Fxc?.GetVS(pass.VS); @@ -145,16 +318,6 @@ private void LoadTechnique(FxcTechnique t) if (gs != null) sb.AppendLine(" geometryShader = " + gs.Name + "();"); if (hs != null) sb.AppendLine(" hullShader = " + hs.Name + "();"); - if ((pass.Params != null) && (pass.Params.Length > 0)) - { - //TODO: properly display the params (what are they all? cbuffers etc) - - //sb.AppendLine(); - //foreach (var param in pass.Params) - //{ - // sb.AppendLine(" " + param.ToString()); - //} - } sb.AppendLine(" }"); } } @@ -166,14 +329,11 @@ private void LoadTechnique(FxcTechnique t) private void ShadersListView_SelectedIndexChanged(object sender, EventArgs e) { - FxcShader s = null; - if (ShadersListView.SelectedItems.Count == 1) - { - s = ShadersListView.SelectedItems[0].Tag as FxcShader; - } - - LoadShader(s); - + if (ShadersListView.SelectedItems.Count != 1) return; + var tag = ShadersListView.SelectedItems[0].Tag; + if (tag is FxcShader fs) LoadShader(fs); + else if (tag is AwcShader awcs) LoadAwcShader(awcs); + else { LoadShader(null); LoadAwcShader((AwcShader)null); } } private void TechniquesListView_SelectedIndexChanged(object sender, EventArgs e) @@ -183,9 +343,151 @@ private void TechniquesListView_SelectedIndexChanged(object sender, EventArgs e) { t = TechniquesListView.SelectedItems[0].Tag as FxcTechnique; } - LoadTechnique(t); + } + + // ---------- AWC: search / filter ---------- + + private void SearchTextBox_TextChanged(object sender, EventArgs e) + { + if (AwcShader != null) RebuildShadersList(); + } + + private void TypeFilterComboBox_SelectedIndexChanged(object sender, EventArgs e) + { + if (AwcShader != null) RebuildShadersList(); + } + + // ---------- AWC: export / import ---------- + + private void ShaderContextMenu_Opening(object sender, System.ComponentModel.CancelEventArgs e) + { + bool hasSelection = AwcShader != null && SelectedAwcShader != null; + ExportCsoMenuItem.Enabled = hasSelection; + ImportCsoMenuItem.Enabled = hasSelection; + if (AwcShader == null) e.Cancel = true; // hide menu entirely in FXC mode + } + + private void ExportCsoMenuItem_Click(object sender, EventArgs e) + { + var s = SelectedAwcShader; + if (s == null) { MessageBox.Show("Select a shader to export."); return; } + using (var sfd = new SaveFileDialog()) + { + sfd.Filter = "Compiled Shader (*.cso)|*.cso|All files (*.*)|*.*"; + sfd.FileName = SafeFileName(s.StageName + "_" + s.Name) + ".cso"; + if (sfd.ShowDialog() != DialogResult.OK) return; + File.WriteAllBytes(sfd.FileName, s.Binary ?? Array.Empty()); + StatusLabel.Text = "Exported " + s.Name + " (" + (s.Binary?.Length ?? 0) + " bytes)"; + } + } + private void ImportCsoMenuItem_Click(object sender, EventArgs e) + { + var s = SelectedAwcShader; + if (s == null) { MessageBox.Show("Select a shader to replace."); return; } + using (var ofd = new OpenFileDialog()) + { + ofd.Filter = "Compiled Shader (*.cso)|*.cso|All files (*.*)|*.*"; + if (ofd.ShowDialog() != DialogResult.OK) return; + byte[] bytes = File.ReadAllBytes(ofd.FileName); + if (bytes.Length < 4) + { + MessageBox.Show("File too small to be a CSO."); + return; + } + uint magic = BitConverter.ToUInt32(bytes, 0); + const uint DXBC = 0x43425844; + const uint DXIL = 0x4C495844; + if (magic != DXBC && magic != DXIL) + { + var r = MessageBox.Show("File does not start with DXBC/DXIL magic. Import anyway?", + "Unrecognised CSO", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + if (r != DialogResult.Yes) return; + } + + int oldSize = (int)s.Size; + s.Binary = bytes; + s.Size = (uint)bytes.Length; + s.BinaryDirty = true; + // Phase 1: keep original metadata block. Game may crash if the new + // CSO's resource layout differs from the original. + + LoadAwcShader(s); + StatusLabel.Text = "Imported " + s.Name + " (" + oldSize + " -> " + bytes.Length + " bytes)"; + } + } + + private void ExportAllMenuItem_Click(object sender, EventArgs e) + { + if (AwcShader == null) return; + using (var fbd = new FolderBrowserDialog()) + { + if (fbd.ShowDialog() != DialogResult.OK) return; + int count = 0; + foreach (var s in AwcShader.AllShaders()) + { + string sub = s.StageName.ToLowerInvariant(); + string dir = Path.Combine(fbd.SelectedPath, sub); + Directory.CreateDirectory(dir); + string path = Path.Combine(dir, SafeFileName(s.Name) + ".cso"); + File.WriteAllBytes(path, s.Binary ?? Array.Empty()); + count++; + } + StatusLabel.Text = "Exported " + count + " shaders to " + fbd.SelectedPath; + } + } + + private static string SafeFileName(string name) + { + if (string.IsNullOrEmpty(name)) return "shader"; + var invalid = Path.GetInvalidFileNameChars(); + var sb = new StringBuilder(name.Length); + foreach (var c in name) sb.Append(Array.IndexOf(invalid, c) >= 0 ? '_' : c); + return sb.ToString(); + } + + // ---------- AWC: save ---------- + + private void SaveMenuItem_Click(object sender, EventArgs e) + { + if (AwcShader == null) return; + if (rpfFileEntry == null) + { + SaveAsMenuItem_Click(sender, e); + return; + } + + try + { + if (!(exploreForm?.EnsureRpfValidEncryption(rpfFileEntry.File) ?? false)) return; + + byte[] data = AwcShader.Save(); + var newentry = RpfFile.CreateFile(rpfFileEntry.Parent, rpfFileEntry.Name, data); + rpfFileEntry = newentry; + AwcShader.FileEntry = newentry; + + exploreForm?.RefreshMainListViewInvoke(); + StatusLabel.Text = "Saved " + rpfFileEntry.Name + " (" + data.Length + " bytes)"; + } + catch (Exception ex) + { + MessageBox.Show("Save failed: " + ex.Message); + } + } + + private void SaveAsMenuItem_Click(object sender, EventArgs e) + { + if (AwcShader == null) return; + using (var sfd = new SaveFileDialog()) + { + sfd.Filter = "AWC Shader Library (*.awc)|*.awc|All files (*.*)|*.*"; + sfd.FileName = fileName ?? "shader.awc"; + if (sfd.ShowDialog() != DialogResult.OK) return; + byte[] data = AwcShader.Save(); + File.WriteAllBytes(sfd.FileName, data); + StatusLabel.Text = "Saved " + Path.GetFileName(sfd.FileName) + " (" + data.Length + " bytes)"; + } } } } diff --git a/CodeWalker/Forms/ShaderDisassembler.cs b/CodeWalker/Forms/ShaderDisassembler.cs new file mode 100644 index 000000000..c3154560b --- /dev/null +++ b/CodeWalker/Forms/ShaderDisassembler.cs @@ -0,0 +1,152 @@ +using CodeWalker.GameFiles; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace CodeWalker.Forms +{ + // Locates dxc.exe / fxc.exe and runs '-dumpbin -Fc' to produce HLSL-style + // disassembly listings. Used by FxcForm for AWC shader library entries. + internal static class ShaderDisassembler + { + private static string _dxcPath; + private static string _fxcPath; + private static bool _dxcResolved; + private static bool _fxcResolved; + + // Local override locations (checked first so a user can drop tools next + // to CodeWalker.exe without touching the SDK). + private static readonly string[] CandidateSubdirs = new[] + { + "", + "tools", + "dxcompilers", + "tools\\dxcompilers", + }; + + public static string DxcPath => _dxcResolved ? _dxcPath : (_dxcPath = Resolve("dxc.exe", out _dxcResolved)); + public static string FxcPath => _fxcResolved ? _fxcPath : (_fxcPath = Resolve("fxc.exe", out _fxcResolved)); + + private static string Resolve(string exeName, out bool resolved) + { + resolved = true; + + foreach (var sub in CandidateSubdirs) + { + var rel = string.IsNullOrEmpty(sub) ? exeName : Path.Combine(sub, exeName); + var p = PathUtil.GetFilePath(rel); + if (File.Exists(p)) return p; + } + + var sdk = ResolveFromWindowsSdk(exeName); + if (sdk != null) return sdk; + + // Let the OS resolve via PATH as a final fallback. + return exeName; + } + + private static string ResolveFromWindowsSdk(string exeName) + { + // Standard Windows 10/11 SDK install layout: + // C:\Program Files (x86)\Windows Kits\10\bin\\\ + string[] roots = + { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Windows Kits", "10", "bin"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Windows Kits", "10", "bin"), + }; + string arch = Environment.Is64BitProcess ? "x64" : "x86"; + + foreach (var root in roots) + { + if (string.IsNullOrEmpty(root) || !Directory.Exists(root)) continue; + + // Prefer the newest 10.0.* sub-directory (lexicographic order works for SDK versions). + var versionDirs = Directory.GetDirectories(root, "10.0.*").OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase); + foreach (var ver in versionDirs) + { + var p = Path.Combine(ver, arch, exeName); + if (File.Exists(p)) return p; + } + + // A few SDK installs drop the binaries directly under bin\. + var direct = Path.Combine(root, arch, exeName); + if (File.Exists(direct)) return direct; + } + return null; + } + + public static string Disassemble(byte[] binary, string shaderName, out string error) + { + error = null; + if (binary == null || binary.Length < 4) + { + error = "Empty shader binary."; + return null; + } + + string tmpIn = Path.Combine(Path.GetTempPath(), "cw_disasm_" + Guid.NewGuid().ToString("N") + ".cso"); + string tmpOut = tmpIn + ".asm"; + try + { + File.WriteAllBytes(tmpIn, binary); + + // Try dxc first (DXIL / SM6+). Fall back to fxc (DXBC / SM5). + var asm = TryRun(DxcPath, "-dumpbin -Fc \"" + tmpOut + "\" \"" + tmpIn + "\"", tmpOut, out var dxcErr); + if (!string.IsNullOrEmpty(asm)) return asm; + + asm = TryRun(FxcPath, "/dumpbin /Fc \"" + tmpOut + "\" \"" + tmpIn + "\"", tmpOut, out var fxcErr); + if (!string.IsNullOrEmpty(asm)) return asm; + + error = "Disassembly failed for " + shaderName + "." + + (string.IsNullOrEmpty(dxcErr) ? string.Empty : "\r\n dxc: " + dxcErr.Trim()) + + (string.IsNullOrEmpty(fxcErr) ? string.Empty : "\r\n fxc: " + fxcErr.Trim()); + return null; + } + catch (Exception ex) + { + error = ex.Message; + return null; + } + finally + { + try { if (File.Exists(tmpIn)) File.Delete(tmpIn); } catch { } + try { if (File.Exists(tmpOut)) File.Delete(tmpOut); } catch { } + } + } + + private static string TryRun(string exe, string args, string outFile, out string error) + { + error = null; + try + { + var psi = new ProcessStartInfo(exe, args) + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + using (var p = Process.Start(psi)) + { + string stdout = p.StandardOutput.ReadToEnd(); + string stderr = p.StandardError.ReadToEnd(); + p.WaitForExit(15000); + + if (File.Exists(outFile)) + { + var text = File.ReadAllText(outFile); + if (!string.IsNullOrWhiteSpace(text)) return text; + } + error = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr; + return null; + } + } + catch (Exception ex) + { + error = exe + ": " + ex.Message; + return null; + } + } + } +} From 6b630373e28e30e313631823ad0abc6ab0f42cb0 Mon Sep 17 00:00:00 2001 From: crxhvrd Date: Fri, 1 May 2026 06:24:39 +0300 Subject: [PATCH 2/3] FxcForm: Gate AWC shader editing behind RPF Explorer's Edit Mode --- CodeWalker/Forms/FxcForm.cs | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/CodeWalker/Forms/FxcForm.cs b/CodeWalker/Forms/FxcForm.cs index 8f35d4af3..bc36bcd1f 100644 --- a/CodeWalker/Forms/FxcForm.cs +++ b/CodeWalker/Forms/FxcForm.cs @@ -36,6 +36,17 @@ public FxcForm() if (TypeFilterComboBox.Items.Count > 0) TypeFilterComboBox.SelectedIndex = 0; UpdateAwcModeUi(awcMode: false); + this.Activated += (s, e) => RefreshEditModeUi(); + } + + private bool IsEditable => AwcShader != null && (exploreForm?.EditMode ?? false); + + private void RefreshEditModeUi() + { + if (AwcShader == null) return; + bool editable = IsEditable; + SaveMenuItem.Enabled = editable && rpfFileEntry != null; + ImportCsoMenuItem.Enabled = editable && SelectedAwcShader != null; } @@ -47,12 +58,12 @@ private void UpdateFormTitle() private void UpdateAwcModeUi(bool awcMode) { - // Menu items only meaningful in AWC mode - SaveMenuItem.Enabled = awcMode; + // Menu items only meaningful in AWC mode. Save / Import are further + // gated on RPF Explorer's edit mode in RefreshEditModeUi() and on + // selection in ShaderContextMenu_Opening. + SaveMenuItem.Enabled = false; SaveAsMenuItem.Enabled = awcMode; ExportAllMenuItem.Enabled = awcMode; - // Per-shader items live on the right-click context menu and are - // gated on selection — see ShaderContextMenu_Opening. // Search/type filter only for AWC (FXC list is small and unsegmented). // SearchPanel is docked Top; toggling visibility lets the docked @@ -132,6 +143,7 @@ public void LoadAwcShader(AwcShaderFile awc, RpfFileEntry entry, ExploreForm own DetailsPropertyGrid.SelectedObject = awc; RebuildShadersList(); + RefreshEditModeUi(); StatusLabel.Text = BuildAwcStatus(); } @@ -364,7 +376,10 @@ private void ShaderContextMenu_Opening(object sender, System.ComponentModel.Canc { bool hasSelection = AwcShader != null && SelectedAwcShader != null; ExportCsoMenuItem.Enabled = hasSelection; - ImportCsoMenuItem.Enabled = hasSelection; + ImportCsoMenuItem.Enabled = hasSelection && IsEditable; + ImportCsoMenuItem.ToolTipText = (hasSelection && !IsEditable) + ? "Enable Edit Mode in RPF Explorer to import shaders." + : null; if (AwcShader == null) e.Cancel = true; // hide menu entirely in FXC mode } @@ -384,6 +399,12 @@ private void ExportCsoMenuItem_Click(object sender, EventArgs e) private void ImportCsoMenuItem_Click(object sender, EventArgs e) { + if (!IsEditable) + { + MessageBox.Show("Enable Edit Mode in RPF Explorer to modify AWC files.", + "Edit Mode required", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } var s = SelectedAwcShader; if (s == null) { MessageBox.Show("Select a shader to replace."); return; } using (var ofd = new OpenFileDialog()) @@ -452,6 +473,12 @@ private static string SafeFileName(string name) private void SaveMenuItem_Click(object sender, EventArgs e) { if (AwcShader == null) return; + if (!IsEditable) + { + MessageBox.Show("Enable Edit Mode in RPF Explorer to save AWC files back to the archive.", + "Edit Mode required", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } if (rpfFileEntry == null) { SaveAsMenuItem_Click(sender, e); From 926fbc29b346bababcb2592b42ba85f4d2ee0c0f Mon Sep 17 00:00:00 2001 From: crxhvrd Date: Sun, 17 May 2026 21:51:12 +0300 Subject: [PATCH 3/3] AwcShaderFile: drop in-form disassembly, simplify viewer to info-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ShaderDisassembler (dxc.exe / fxc.exe shellout) — locating the Windows SDK shader compilers was unreliable across user installs and the async disassembly path added UI complexity for a feature that often returned an error string. The AWC shader viewer now renders only the parsed header block (stage/name, hash, sizes, register & CBuffer listing), which is fully derivable from the SGD2 parse with no external dependency. FXC shader disassembly is unaffected — that path uses SharpDX.D3DCompiler in-process via FxcParser. Also strip cross-workspace and ported-from comments from AwcShaderFile and FxcForm so the source stands on its own. --- .../GameFiles/FileTypes/AwcShaderFile.cs | 11 +- CodeWalker/Forms/FxcForm.cs | 25 +-- CodeWalker/Forms/ShaderDisassembler.cs | 152 ------------------ 3 files changed, 7 insertions(+), 181 deletions(-) delete mode 100644 CodeWalker/Forms/ShaderDisassembler.cs diff --git a/CodeWalker.Core/GameFiles/FileTypes/AwcShaderFile.cs b/CodeWalker.Core/GameFiles/FileTypes/AwcShaderFile.cs index 75d4123dc..0bc67c6dd 100644 --- a/CodeWalker.Core/GameFiles/FileTypes/AwcShaderFile.cs +++ b/CodeWalker.Core/GameFiles/FileTypes/AwcShaderFile.cs @@ -9,8 +9,7 @@ // AWC Shader Library (SGD2 / Shader Group Data v2) reader & writer. // Used by GTA V Enhanced (Gen9) compiled-shader containers. Distinct from the // audio Audio Wave Container (AwcFile.cs / magic ADAT) which shares the .awc -// extension. Ported from the Python reference parser at -// GTATOOLS/fxc/shadermanager/awclib (parser.py, models.py, awc_writer.py). +// extension. namespace CodeWalker.GameFiles { @@ -312,8 +311,7 @@ private static AwcShaderRegister ParseRegister(MemoryStream ms, BinaryReader br) long afterHeader = ms.Position; // headerStart + 12 byte[] extra = br.ReadBytes(16); - // Offsets in the binary are relative to headerStart (the parser in - // Python expresses this as (afterHeader + offset - 12)). + // Offsets in the binary are relative to headerStart. string regName; long savedPos = ms.Position; ms.Position = headerStart + regStringOff; @@ -467,9 +465,8 @@ private static void WriteShader(BinaryWriter bw, AwcShader s) bw.Write(block); } - // Mirrors awc_writer.py:_build_metadata_block. Unused in Phase 1 (binary- - // only imports keep MetadataDirty=false), but kept so the data model is - // round-trippable end-to-end. + // Unused while MetadataDirty stays false (binary-only imports), but kept + // so the data model is round-trippable end-to-end. private static byte[] BuildMetadataBlock(AwcShader s) { var regs = s.Registers ?? Array.Empty(); diff --git a/CodeWalker/Forms/FxcForm.cs b/CodeWalker/Forms/FxcForm.cs index bc36bcd1f..1c60cd349 100644 --- a/CodeWalker/Forms/FxcForm.cs +++ b/CodeWalker/Forms/FxcForm.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; namespace CodeWalker.Forms @@ -243,25 +242,7 @@ private void LoadAwcShader(AwcShader s) } ShaderPanel.Enabled = true; - var header = BuildShaderHeader(s); - ShaderTextBox.Text = header + "\r\n// Disassembling... (dxc -dumpbin)\r\n"; - - // dxc/fxc takes 50-500ms — keep the UI responsive. - var binary = s.Binary; - var name = s.Name; - var stage = s.StageName; - Task.Run(() => - { - string asm = ShaderDisassembler.Disassemble(binary, name, out string err); - string body = !string.IsNullOrEmpty(asm) - ? asm - : "// Disassembly unavailable.\r\n// " + (err ?? "Unknown error").Replace("\n", "\n// "); - BeginInvoke((Action)(() => - { - if (SelectedAwcShader == null || !ReferenceEquals(SelectedAwcShader.Binary, binary)) return; - ShaderTextBox.Text = header + "\r\n" + body; - })); - }); + ShaderTextBox.Text = BuildShaderHeader(s); } private static string BuildShaderHeader(AwcShader s) @@ -431,8 +412,8 @@ private void ImportCsoMenuItem_Click(object sender, EventArgs e) s.Binary = bytes; s.Size = (uint)bytes.Length; s.BinaryDirty = true; - // Phase 1: keep original metadata block. Game may crash if the new - // CSO's resource layout differs from the original. + // Keep original metadata block — game may crash if the new CSO's + // resource layout differs from the original. LoadAwcShader(s); StatusLabel.Text = "Imported " + s.Name + " (" + oldSize + " -> " + bytes.Length + " bytes)"; diff --git a/CodeWalker/Forms/ShaderDisassembler.cs b/CodeWalker/Forms/ShaderDisassembler.cs deleted file mode 100644 index c3154560b..000000000 --- a/CodeWalker/Forms/ShaderDisassembler.cs +++ /dev/null @@ -1,152 +0,0 @@ -using CodeWalker.GameFiles; -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; - -namespace CodeWalker.Forms -{ - // Locates dxc.exe / fxc.exe and runs '-dumpbin -Fc' to produce HLSL-style - // disassembly listings. Used by FxcForm for AWC shader library entries. - internal static class ShaderDisassembler - { - private static string _dxcPath; - private static string _fxcPath; - private static bool _dxcResolved; - private static bool _fxcResolved; - - // Local override locations (checked first so a user can drop tools next - // to CodeWalker.exe without touching the SDK). - private static readonly string[] CandidateSubdirs = new[] - { - "", - "tools", - "dxcompilers", - "tools\\dxcompilers", - }; - - public static string DxcPath => _dxcResolved ? _dxcPath : (_dxcPath = Resolve("dxc.exe", out _dxcResolved)); - public static string FxcPath => _fxcResolved ? _fxcPath : (_fxcPath = Resolve("fxc.exe", out _fxcResolved)); - - private static string Resolve(string exeName, out bool resolved) - { - resolved = true; - - foreach (var sub in CandidateSubdirs) - { - var rel = string.IsNullOrEmpty(sub) ? exeName : Path.Combine(sub, exeName); - var p = PathUtil.GetFilePath(rel); - if (File.Exists(p)) return p; - } - - var sdk = ResolveFromWindowsSdk(exeName); - if (sdk != null) return sdk; - - // Let the OS resolve via PATH as a final fallback. - return exeName; - } - - private static string ResolveFromWindowsSdk(string exeName) - { - // Standard Windows 10/11 SDK install layout: - // C:\Program Files (x86)\Windows Kits\10\bin\\\ - string[] roots = - { - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Windows Kits", "10", "bin"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Windows Kits", "10", "bin"), - }; - string arch = Environment.Is64BitProcess ? "x64" : "x86"; - - foreach (var root in roots) - { - if (string.IsNullOrEmpty(root) || !Directory.Exists(root)) continue; - - // Prefer the newest 10.0.* sub-directory (lexicographic order works for SDK versions). - var versionDirs = Directory.GetDirectories(root, "10.0.*").OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase); - foreach (var ver in versionDirs) - { - var p = Path.Combine(ver, arch, exeName); - if (File.Exists(p)) return p; - } - - // A few SDK installs drop the binaries directly under bin\. - var direct = Path.Combine(root, arch, exeName); - if (File.Exists(direct)) return direct; - } - return null; - } - - public static string Disassemble(byte[] binary, string shaderName, out string error) - { - error = null; - if (binary == null || binary.Length < 4) - { - error = "Empty shader binary."; - return null; - } - - string tmpIn = Path.Combine(Path.GetTempPath(), "cw_disasm_" + Guid.NewGuid().ToString("N") + ".cso"); - string tmpOut = tmpIn + ".asm"; - try - { - File.WriteAllBytes(tmpIn, binary); - - // Try dxc first (DXIL / SM6+). Fall back to fxc (DXBC / SM5). - var asm = TryRun(DxcPath, "-dumpbin -Fc \"" + tmpOut + "\" \"" + tmpIn + "\"", tmpOut, out var dxcErr); - if (!string.IsNullOrEmpty(asm)) return asm; - - asm = TryRun(FxcPath, "/dumpbin /Fc \"" + tmpOut + "\" \"" + tmpIn + "\"", tmpOut, out var fxcErr); - if (!string.IsNullOrEmpty(asm)) return asm; - - error = "Disassembly failed for " + shaderName + "." - + (string.IsNullOrEmpty(dxcErr) ? string.Empty : "\r\n dxc: " + dxcErr.Trim()) - + (string.IsNullOrEmpty(fxcErr) ? string.Empty : "\r\n fxc: " + fxcErr.Trim()); - return null; - } - catch (Exception ex) - { - error = ex.Message; - return null; - } - finally - { - try { if (File.Exists(tmpIn)) File.Delete(tmpIn); } catch { } - try { if (File.Exists(tmpOut)) File.Delete(tmpOut); } catch { } - } - } - - private static string TryRun(string exe, string args, string outFile, out string error) - { - error = null; - try - { - var psi = new ProcessStartInfo(exe, args) - { - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - using (var p = Process.Start(psi)) - { - string stdout = p.StandardOutput.ReadToEnd(); - string stderr = p.StandardError.ReadToEnd(); - p.WaitForExit(15000); - - if (File.Exists(outFile)) - { - var text = File.ReadAllText(outFile); - if (!string.IsNullOrWhiteSpace(text)) return text; - } - error = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr; - return null; - } - } - catch (Exception ex) - { - error = exe + ": " + ex.Message; - return null; - } - } - } -}