11using System ;
22using System . Collections . Generic ;
3+ using System . IO ;
34using System . Text ;
45using CommunityToolkit . HighPerformance ;
56using Silk . NET . SPIRV ;
67using Silk . NET . SPIRV . Cross ;
8+ using Stride . Core . IO ;
9+ using Stride . Core . Storage ;
710using Stride . Shaders . Compilers ;
811using Stride . Shaders . Compilers . SDSL ;
12+ using Stride . Shaders . Spirv . Building ;
913using Stride . Shaders . Spirv . Core . Buffers ;
1014using Stride . Shaders . Spirv . Tools ;
1115using Spv = Stride . Shaders . Spirv . Tools . Spv ;
@@ -14,6 +18,90 @@ namespace Stride.Shaders.Parsers.Tests;
1418
1519public class StrideShaderTests
1620{
21+ // Regression: the MemberName re-instantiation path sets ShaderLoaderBase.SuppressSourceHash and
22+ // relies on LoadFromCode to clear it. When the load hits the shader cache instead, LoadFromCode
23+ // never runs, so the flag leaks into the next compiled shader and strips its OpSourceHashSDSL.
24+ // A cached shader without OpSourceHashSDSL deserializes to a zero hash, which makes
25+ // EffectCompilerCache.IsBytecodeObsolete return true on every run (a permanent recompile hang).
26+ [ Fact ]
27+ public void SuppressSourceHashDoesNotLeakAcrossCacheHits ( )
28+ {
29+ var loader = new ShaderLoader ( "./assets/Stride/SDSL" ) ;
30+
31+ // Warm the cache: a normally compiled shader carries its OpSourceHashSDSL.
32+ Assert . True ( loader . LoadExternalBuffer ( "Texturing" , [ ] , out var texturing , out _ , out _ ) ) ;
33+ Assert . True ( HasSourceHash ( texturing ) , "sanity: a normally compiled shader carries OpSourceHashSDSL" ) ;
34+
35+ // Reproduce the MemberName path (Builder.Class.InstantiateMemberNames): set the flag, then load
36+ // a shader that is already cached. The (name, filename, code) overload returns on the cache hit
37+ // before LoadFromCode runs, so the flag is never cleared.
38+ Assert . True ( loader . LoadExternalFileContent ( "Texturing" , out var filename , out var code , out _ ) ) ;
39+ loader . SuppressSourceHash = true ;
40+ Assert . True ( loader . LoadExternalBuffer ( "Texturing" , filename , code , [ ] , out _ , out _ , out var isFromCache ) ) ;
41+ Assert . True ( isFromCache , "precondition: the second load must be a cache hit to exercise the leak" ) ;
42+
43+ Assert . False ( loader . SuppressSourceHash ,
44+ "SuppressSourceHash leaked past a cache hit; the next compiled shader would be cached without " +
45+ "OpSourceHashSDSL (zero hash => EffectCompilerCache recompiles every run)" ) ;
46+
47+ // Downstream symptom: with the flag leaked, the next freshly compiled shader loses its hash.
48+ Assert . True ( loader . LoadExternalBuffer ( "ShaderBase" , [ ] , out var shaderBase , out _ , out _ ) ) ;
49+ Assert . True ( HasSourceHash ( shaderBase ) ,
50+ "the next compiled shader is missing OpSourceHashSDSL — its cached hash would deserialize to zero" ) ;
51+ }
52+
53+ private static bool HasSourceHash ( ShaderBuffers buffer )
54+ {
55+ foreach ( var i in buffer . Context )
56+ {
57+ if ( i . Op == Stride . Shaders . Spirv . Specification . Op . OpSourceHashSDSL )
58+ return true ;
59+ }
60+ return false ;
61+ }
62+
63+ // FileShaderCache stamps a Generator id + Schema (format version) into the SPIR-V header of each
64+ // cached .spv. A cached buffer round-trips (including its OpSourceHashSDSL), and a header whose
65+ // version no longer matches is rejected so the shader recompiles instead of being served stale.
66+ [ Fact ]
67+ public void FileShaderCacheRoundTripsAndRejectsStaleVersion ( )
68+ {
69+ var loader = new ShaderLoader ( "./assets/Stride/SDSL" ) ;
70+ Assert . True ( loader . LoadExternalBuffer ( "Texturing" , [ ] , out var buffer , out var hash , out _ ) ) ;
71+ Assert . NotEqual ( ObjectId . Empty , hash ) ;
72+
73+ var tempDir = Directory . CreateTempSubdirectory ( "stride-shadercache-test" ) ;
74+ try
75+ {
76+ var provider = new FileSystemProvider ( "/testcache" , tempDir . FullName ) ;
77+ const string spvPath = "shaders/Texturing_default.spv" ;
78+
79+ // Write via one instance, read via a fresh instance so the load must come from disk.
80+ new FileShaderCache ( provider ) . RegisterShader ( "Texturing" , null , [ ] , buffer , hash ) ;
81+
82+ Assert . True ( new FileShaderCache ( provider ) . TryLoadFromCache ( "Texturing" , null , [ ] , out _ , out var loadedHash ) ) ;
83+ Assert . Equal ( hash , loadedHash ) ;
84+
85+ // Corrupt the Schema word (5th header word, byte offset 16) to simulate an old/incompatible cache.
86+ byte [ ] bytes ;
87+ using ( var s = provider . OpenStream ( spvPath , VirtualFileMode . Open , VirtualFileAccess . Read ) )
88+ {
89+ bytes = new byte [ s . Length ] ;
90+ s . ReadExactly ( bytes ) ;
91+ }
92+ BitConverter . GetBytes ( 0x7FFFFFFF ) . CopyTo ( bytes , 16 ) ;
93+ using ( var s = provider . OpenStream ( spvPath , VirtualFileMode . Create , VirtualFileAccess . Write ) )
94+ s . Write ( bytes ) ;
95+
96+ Assert . False ( new FileShaderCache ( provider ) . TryLoadFromCache ( "Texturing" , null , [ ] , out _ , out _ ) ,
97+ "a cache file with a mismatched format version must be rejected (recompiled), not served stale" ) ;
98+ }
99+ finally
100+ {
101+ tempDir . Delete ( recursive : true ) ;
102+ }
103+ }
104+
17105 [ Fact ]
18106 public void TextureDecorateStringWarning ( )
19107 {
0 commit comments