From d556f23e3cf538e392f20fe9f52e8809a0f3eb64 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 01:44:33 +0000 Subject: [PATCH 1/2] write stub data for texture coord semantics above index 0 Authored by Claude (claude-opus-5) Previously only the UByte4N decl type got stub bytes when a texture coordinate element had a semantic index > 0; every other type was skipped with a warning and nothing was written. If such an element was the last one in the vertex, the vertex ended up short and writeVertex failed with "Wrote an insufficient number of bytes for the vertex", which aborted the whole fill and prevented the mod from loading. This was hit by a d3d11 layout with TEXCOORD1 as R32G32_Float (wrote 40 of 48 bytes). Now a stub of the correct size is written for all texture coordinate types that are supported at semantic index 0, using a mid-range UV (0.5,0.5) and zeros for any extra components. Unknown types are zero filled using the element size; if even that can't be determined, nothing is written (the old behavior). The existing UByte4N stub is unchanged. The warning is now emitted once per fill from setup rather than from the per-vertex loop, so the stub bytes can also be precomputed there instead of being rebuilt for every vertex. Also factors the format size table out of MeshUtil.getVertSizeFromEls into getSizeFromFormat/getSizeFromElType so the stub code can use it. --- MMManaged/MeshUtil.fs | 39 ++++++++++++--------- MMManaged/ModDBInterop.fs | 74 +++++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 36 deletions(-) diff --git a/MMManaged/MeshUtil.fs b/MMManaged/MeshUtil.fs index 5bb478b..a2eb196 100644 --- a/MMManaged/MeshUtil.fs +++ b/MMManaged/MeshUtil.fs @@ -502,26 +502,31 @@ map_Kd $$filename int hElement.Offset + sizeBytes + let getSizeFromFormat (f:SharpDX.DXGI.Format) = + match f with + | SharpDX.DXGI.Format.R32_Float -> 4 + | SharpDX.DXGI.Format.R32G32_Float -> 8 + | SharpDX.DXGI.Format.R32G32B32_Float -> 12 + | SharpDX.DXGI.Format.R32G32B32A32_Float -> 16 + | SharpDX.DXGI.Format.R16G16_Float -> 4 + | SharpDX.DXGI.Format.R16G16_SNorm -> 4 + | SharpDX.DXGI.Format.R16G16B16A16_Float -> 8 + | SharpDX.DXGI.Format.R8G8B8A8_UInt -> 4 + | SharpDX.DXGI.Format.B8G8R8A8_UNorm -> 4 + | SharpDX.DXGI.Format.R16G16B16A16_SInt -> 8 + | SharpDX.DXGI.Format.R16G16B16A16_SNorm -> 8 + | _ -> failwithf "Some lazy person didn't fill in the size of format type %A" f + + /// Returns the size (in bytes) of a single vertex element. + let getSizeFromElType (elType:VertexTypes.MMVertexElementType) = + match elType with + | VertexTypes.MMVertexElementType.DeclType(dt) -> getSizeFromDeclType dt + | VertexTypes.MMVertexElementType.Format(f) -> getSizeFromFormat f + let getVertSizeFromEls(elements:VertexTypes.MMVertexElement []) = let hElement = elements |> Array.maxBy (fun el -> el.Offset) - let sizeBytes = - match hElement.Type with - | VertexTypes.MMVertexElementType.DeclType(dt) -> getSizeFromDeclType dt - | VertexTypes.MMVertexElementType.Format(f) -> - match f with - | SharpDX.DXGI.Format.R32_Float -> 4 - | SharpDX.DXGI.Format.R32G32_Float -> 8 - | SharpDX.DXGI.Format.R32G32B32_Float -> 12 - | SharpDX.DXGI.Format.R32G32B32A32_Float -> 16 - | SharpDX.DXGI.Format.R16G16_Float -> 4 - | SharpDX.DXGI.Format.R16G16_SNorm -> 4 - | SharpDX.DXGI.Format.R16G16B16A16_Float -> 8 - | SharpDX.DXGI.Format.R8G8B8A8_UInt -> 4 - | SharpDX.DXGI.Format.B8G8R8A8_UNorm -> 4 - | SharpDX.DXGI.Format.R16G16B16A16_SInt -> 8 - | SharpDX.DXGI.Format.R16G16B16A16_SNorm -> 8 - | _ -> failwithf "Some lazy person didn't fill in the size of format type %A" f + let sizeBytes = getSizeFromElType hElement.Type int hElement.Offset + sizeBytes /// Returns true if the declaration list contains blend data, false otherwise. diff --git a/MMManaged/ModDBInterop.fs b/MMManaged/ModDBInterop.fs index ff2eaf5..a2e5406 100644 --- a/MMManaged/ModDBInterop.fs +++ b/MMManaged/ModDBInterop.fs @@ -1047,11 +1047,56 @@ module ModDBInterop = | Some bvd -> bvd RawBinaryWriters.rbBinormalTangent binDataLookup vertRels - // lazy variants: these fire from the per-vertex hot loop, so - // the message thunk avoids sprintf/%A cost on every call - let tex1SemUnused = Logging.logOnce(0) - let tex1UnormSub = Logging.logOnce(0) - + // We don't track source data for texture coordinate semantics above index 0, but + // something must still be written for them: if the element is skipped the vertex + // ends up short, which trips the size check in writeVertex and prevents the mod + // from loading at all. So write stub data of the correct size instead. The UV + // components get a mid-range value (0.5,0.5) and any extra components are zeroed; + // rendering may be somewhat wrong but the mod loads. + let makeTexCoordStub (elType:MMET): byte[] = + let half (f:float32) = System.BitConverter.GetBytes(MonoGameHelpers.floatToHalfUint16 f) + let flt (f:float32) = System.BitConverter.GetBytes(f) + let snorm (f:float32) = System.BitConverter.GetBytes(int16(f * 32767.f)) + match elType with + | MMET.DeclType(dt) when dt = SDXVT.Unused -> [||] + | MMET.DeclType(dt) when dt = SDXVT.UByte4N -> + // this stub predates the general handling here; keep it as is since it is + // known to work in at least one game + [| byte 16; byte 128; byte 128; byte 128 |] + | MMET.DeclType(dt) when dt = SDXVT.Float2 -> Array.concat [ flt 0.5f; flt 0.5f ] + | MMET.DeclType(dt) when dt = SDXVT.HalfTwo -> Array.concat [ half 0.5f; half 0.5f ] + | MMET.DeclType(dt) when dt = SDXVT.HalfFour -> + Array.concat [ half 0.5f; half 0.5f; half 0.f; half 0.f ] + | MMET.Format(f) when f = SDXF.R32G32_Float -> Array.concat [ flt 0.5f; flt 0.5f ] + | MMET.Format(f) when f = SDXF.R16G16_Float -> + let h (x:float32) = System.BitConverter.GetBytes(SharpDX.Half(x).RawValue) + Array.concat [ h 0.5f; h 0.5f ] + | MMET.Format(f) when f = SDXF.R16G16B16A16_SNorm -> + Array.concat [ snorm 0.5f; snorm 0.5f; snorm 0.f; snorm 0.f ] + | other -> + // unknown type: just zero fill it, which at least keeps the vertex size correct. + // if even the size is unknown, write nothing, as older code did; the vert size + // check may then fail, but that's no worse than throwing here. + try + Array.zeroCreate (MeshUtil.getSizeFromElType other) + with e -> + log.Warn "can't determine size of texture coordinate element type %A, no stub data will be written: %s" + other e.Message + [||] + + // Precompute the stubs (keyed on semantic index) so the per-vertex hot loop doesn't + // have to build them, and warn about each one here rather than from that loop. + let texCoordStubs = new System.Collections.Generic.Dictionary() + declElements + |> Array.filter (fun el -> + el.Semantic = MMVertexElemSemantic.TextureCoordinate && el.SemanticIndex > 0) + |> Array.iter (fun el -> + let stub = makeTexCoordStub el.Type + if stub.Length > 0 then + log.Warn "no source data available for texture coordinate semantic index %d (type %A); writing %d bytes of stub data: %A" + el.SemanticIndex el.Type stub.Length stub + texCoordStubs.[el.SemanticIndex] <- stub) + // Write part of a vertex. The input element controls which // part is written. let writeElement (v:PTNIndex) (el:VertexTypes.MMVertexElement) = @@ -1117,20 +1162,11 @@ module ModDBInterop = // remaining two shorts unused for now but maybe not in general case () | _ -> failwithf "Unsupported type for texture coordinate: %A" el.Type - else - match el.Type with - | MMET.DeclType(dt) when dt = SDXVT.UByte4N -> - // I don't track this data currently but write some stub bytes so the mod at least loads; if we don't - // write anything the vert size check will complain about an insufficient number of bytes - let stubBytes = [| byte 16; byte 128; byte 128; byte 128 |] - tex1UnormSub (fun () -> - sprintf "warning: writing stub bytes %A for tex coord semantic index %d (format %A); no source data available" stubBytes el.SemanticIndex el.Type) - bw.Write(stubBytes) - | _ -> - tex1SemUnused (fun () -> - sprintf "warning: texture coord semantic index > 0 is ignored: index: %A; format: %A" - el.SemanticIndex el.Type) - + else + // no source data for this semantic; write the stub computed above + // (already warned about at that point) + bw.Write(texCoordStubs.[el.SemanticIndex]) + | MMVertexElemSemantic.Normal -> normalWriter modNrmIndex modVertIndex el bw | MMVertexElemSemantic.Binormal | MMVertexElemSemantic.Tangent -> binormalTangentWriter modNrmIndex modVertIndex el bw From 92e51c7cd4d644372a323390b933f7154e455b1e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 20:25:06 +0000 Subject: [PATCH 2/2] warn about tex coord stub data on every mod load Authored by Claude (claude-opus-5) The stub-data warning was emitted from the vertex fill, which is skipped entirely on a VBData disk cache hit. So it only appeared the first time a mod was loaded, and after that the fact that part of the vertex data is fake -- and that rendering may therefore be somewhat off -- was invisible in the log. Move the stub construction (makeTexCoordStub, plus a new buildTexCoordStubs that warns and returns the per-semantic-index map) to module scope and run it in fillModDataInternalHelper before the cache check, so the warning is logged on every load regardless of which path fills the buffer. The warning now also names the mod and says that rendering may be incorrect. --- MMManaged/ModDBInterop.fs | 113 ++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 52 deletions(-) diff --git a/MMManaged/ModDBInterop.fs b/MMManaged/ModDBInterop.fs index a2e5406..c5544e4 100644 --- a/MMManaged/ModDBInterop.fs +++ b/MMManaged/ModDBInterop.fs @@ -767,6 +767,59 @@ module ModDBInterop = WriteD3D9Decl of (BinaryWriter * int) | ReadD3D11Layout of (MMVertexElement []) + /// We don't track source data for texture coordinate semantics above index 0, but something must + /// still be written for them: if the element is skipped the vertex ends up short, which trips the + /// size check in the vertex writer and prevents the mod from loading at all. So write stub data + /// of the correct size instead. The UV components get a mid-range value (0.5,0.5) and any extra + /// components are zeroed. + let private makeTexCoordStub (elType:MMET): byte[] = + let half (f:float32) = System.BitConverter.GetBytes(MonoGameHelpers.floatToHalfUint16 f) + let flt (f:float32) = System.BitConverter.GetBytes(f) + let snorm (f:float32) = System.BitConverter.GetBytes(int16(f * 32767.f)) + match elType with + | MMET.DeclType(dt) when dt = SDXVT.Unused -> [||] + | MMET.DeclType(dt) when dt = SDXVT.UByte4N -> + // this stub predates the general handling here; keep it as is since it is + // known to work in at least one game + [| byte 16; byte 128; byte 128; byte 128 |] + | MMET.DeclType(dt) when dt = SDXVT.Float2 -> Array.concat [ flt 0.5f; flt 0.5f ] + | MMET.DeclType(dt) when dt = SDXVT.HalfTwo -> Array.concat [ half 0.5f; half 0.5f ] + | MMET.DeclType(dt) when dt = SDXVT.HalfFour -> + Array.concat [ half 0.5f; half 0.5f; half 0.f; half 0.f ] + | MMET.Format(f) when f = SDXF.R32G32_Float -> Array.concat [ flt 0.5f; flt 0.5f ] + | MMET.Format(f) when f = SDXF.R16G16_Float -> + let h (x:float32) = System.BitConverter.GetBytes(SharpDX.Half(x).RawValue) + Array.concat [ h 0.5f; h 0.5f ] + | MMET.Format(f) when f = SDXF.R16G16B16A16_SNorm -> + Array.concat [ snorm 0.5f; snorm 0.5f; snorm 0.f; snorm 0.f ] + | other -> + // unknown type: just zero fill it, which at least keeps the vertex size correct. + // if even the size is unknown, write nothing, as older code did; the vert size + // check may then fail, but that's no worse than throwing here. + try + Array.zeroCreate (MeshUtil.getSizeFromElType other) + with e -> + log.Warn "can't determine size of texture coordinate element type %A, no stub data will be written: %s" + other e.Message + [||] + + /// Build the stub data (keyed on semantic index) for all texture coordinate elements above + /// semantic index 0, warning about each one. Callers should do this on every mod load, even + /// when the fill itself is skipped (VBData cache hit), so that it is always apparent from the + /// log that part of the vertex data is fake and rendering may therefore be somewhat incorrect. + let private buildTexCoordStubs (modName:string) (elements:MMVertexElement []) = + let stubs = new System.Collections.Generic.Dictionary() + elements + |> Array.filter (fun el -> + el.Semantic = MMVertexElemSemantic.TextureCoordinate && el.SemanticIndex > 0) + |> Array.iter (fun el -> + let stub = makeTexCoordStub el.Type + if stub.Length > 0 then + log.Warn "mod %A: no source data available for texture coordinate semantic index %d (type %A); using %d bytes of stub data (%A), rendering may be somewhat incorrect" + modName el.SemanticIndex el.Type stub.Length stub + stubs.[el.SemanticIndex] <- stub) + stubs + /// Fill the render buffers associated with the specified mod. // Note: there is a lot of symmetry between this and the snapshot module (essentially they are the same // process in two different directions), but they have totally separate implementations right now. Might be worth @@ -823,6 +876,12 @@ module ModDBInterop = let elStr = vertElsToString elements "d3d11", [||], elements, VBDataDiskCache.hashString elStr, 0 + // Build stub data for any texture coordinate semantic we have no source data for. + // Done here, before the VBData cache check below, so that the warnings it logs appear on + // every load of the mod: a cache hit skips the fill entirely, and the stub data baked + // into the cached bytes would otherwise be invisible in the log. + let texCoordStubs = buildTexCoordStubs meshrel.DBMod.Name declElements + // copy index data...someday if (destIbSize > 0) then failwith "Filling index data is not yet supported" @@ -1047,56 +1106,6 @@ module ModDBInterop = | Some bvd -> bvd RawBinaryWriters.rbBinormalTangent binDataLookup vertRels - // We don't track source data for texture coordinate semantics above index 0, but - // something must still be written for them: if the element is skipped the vertex - // ends up short, which trips the size check in writeVertex and prevents the mod - // from loading at all. So write stub data of the correct size instead. The UV - // components get a mid-range value (0.5,0.5) and any extra components are zeroed; - // rendering may be somewhat wrong but the mod loads. - let makeTexCoordStub (elType:MMET): byte[] = - let half (f:float32) = System.BitConverter.GetBytes(MonoGameHelpers.floatToHalfUint16 f) - let flt (f:float32) = System.BitConverter.GetBytes(f) - let snorm (f:float32) = System.BitConverter.GetBytes(int16(f * 32767.f)) - match elType with - | MMET.DeclType(dt) when dt = SDXVT.Unused -> [||] - | MMET.DeclType(dt) when dt = SDXVT.UByte4N -> - // this stub predates the general handling here; keep it as is since it is - // known to work in at least one game - [| byte 16; byte 128; byte 128; byte 128 |] - | MMET.DeclType(dt) when dt = SDXVT.Float2 -> Array.concat [ flt 0.5f; flt 0.5f ] - | MMET.DeclType(dt) when dt = SDXVT.HalfTwo -> Array.concat [ half 0.5f; half 0.5f ] - | MMET.DeclType(dt) when dt = SDXVT.HalfFour -> - Array.concat [ half 0.5f; half 0.5f; half 0.f; half 0.f ] - | MMET.Format(f) when f = SDXF.R32G32_Float -> Array.concat [ flt 0.5f; flt 0.5f ] - | MMET.Format(f) when f = SDXF.R16G16_Float -> - let h (x:float32) = System.BitConverter.GetBytes(SharpDX.Half(x).RawValue) - Array.concat [ h 0.5f; h 0.5f ] - | MMET.Format(f) when f = SDXF.R16G16B16A16_SNorm -> - Array.concat [ snorm 0.5f; snorm 0.5f; snorm 0.f; snorm 0.f ] - | other -> - // unknown type: just zero fill it, which at least keeps the vertex size correct. - // if even the size is unknown, write nothing, as older code did; the vert size - // check may then fail, but that's no worse than throwing here. - try - Array.zeroCreate (MeshUtil.getSizeFromElType other) - with e -> - log.Warn "can't determine size of texture coordinate element type %A, no stub data will be written: %s" - other e.Message - [||] - - // Precompute the stubs (keyed on semantic index) so the per-vertex hot loop doesn't - // have to build them, and warn about each one here rather than from that loop. - let texCoordStubs = new System.Collections.Generic.Dictionary() - declElements - |> Array.filter (fun el -> - el.Semantic = MMVertexElemSemantic.TextureCoordinate && el.SemanticIndex > 0) - |> Array.iter (fun el -> - let stub = makeTexCoordStub el.Type - if stub.Length > 0 then - log.Warn "no source data available for texture coordinate semantic index %d (type %A); writing %d bytes of stub data: %A" - el.SemanticIndex el.Type stub.Length stub - texCoordStubs.[el.SemanticIndex] <- stub) - // Write part of a vertex. The input element controls which // part is written. let writeElement (v:PTNIndex) (el:VertexTypes.MMVertexElement) = @@ -1163,8 +1172,8 @@ module ModDBInterop = () | _ -> failwithf "Unsupported type for texture coordinate: %A" el.Type else - // no source data for this semantic; write the stub computed above - // (already warned about at that point) + // no source data for this semantic; write the stub built (and + // warned about) at the top of this function bw.Write(texCoordStubs.[el.SemanticIndex]) | MMVertexElemSemantic.Normal -> normalWriter modNrmIndex modVertIndex el bw