From a1568d13a40075c96db92f2cde396bf4aa6c06f0 Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 13 May 2026 10:39:59 -0700 Subject: [PATCH 01/16] Add initial binding array tests --- src/test/compile.sh | 1 + .../buffer_binding_array.frag | 19 +++++++++++++ .../buffer_binding_array.spv | Bin 0 -> 1288 bytes src/test/splitbindingarray/compile.sh | 6 +++++ .../nested_texture_binding_array.frag | 25 ++++++++++++++++++ .../nested_texture_binding_array.spv | Bin 0 -> 1464 bytes .../storage_binding_array.frag | 19 +++++++++++++ .../storage_binding_array.spv | Bin 0 -> 1288 bytes .../texture_binding_array.frag | 16 +++++++++++ .../texture_binding_array.spv | Bin 0 -> 1200 bytes 10 files changed, 86 insertions(+) create mode 100644 src/test/splitbindingarray/buffer_binding_array.frag create mode 100644 src/test/splitbindingarray/buffer_binding_array.spv create mode 100755 src/test/splitbindingarray/compile.sh create mode 100644 src/test/splitbindingarray/nested_texture_binding_array.frag create mode 100644 src/test/splitbindingarray/nested_texture_binding_array.spv create mode 100644 src/test/splitbindingarray/storage_binding_array.frag create mode 100644 src/test/splitbindingarray/storage_binding_array.spv create mode 100644 src/test/splitbindingarray/texture_binding_array.frag create mode 100644 src/test/splitbindingarray/texture_binding_array.spv diff --git a/src/test/compile.sh b/src/test/compile.sh index 28bca60..f4d057b 100755 --- a/src/test/compile.sh +++ b/src/test/compile.sh @@ -7,3 +7,4 @@ set -e (cd storagecubepatch; ./compile.sh) (cd pruneunuseddref; ./compile.sh) (cd immediatespatch; ./compile.sh) +(cd splitbindingarray; ./compile.sh) diff --git a/src/test/splitbindingarray/buffer_binding_array.frag b/src/test/splitbindingarray/buffer_binding_array.frag new file mode 100644 index 0000000..946590c --- /dev/null +++ b/src/test/splitbindingarray/buffer_binding_array.frag @@ -0,0 +1,19 @@ +#version 440 + +#define MAX_THINGS 8 + +layout(location = 0) out vec4 o_color; + +layout(set = 0, binding = 0, std140) uniform Thing { + vec4 thing1; + float thing2; +} u_things[MAX_THINGS]; + +void main() { + o_color = vec4(0.0); + for (int i = 0; i < MAX_THINGS; i++) { + o_color += u_things[i].thing1; + o_color.x += u_things[i].thing2; + } +} + diff --git a/src/test/splitbindingarray/buffer_binding_array.spv b/src/test/splitbindingarray/buffer_binding_array.spv new file mode 100644 index 0000000000000000000000000000000000000000..a2eac47f3e01113e0cb1a11a651140868db24a68 GIT binary patch literal 1288 zcmYk6TWb?R7=|Zp5^JkH*pr7^H;w1kctAxEgv3>$c*JM zUnH8dLQWkkKE2OJ2F`<3a^z^vZk~=I{Ggy77DL+2(xaoa`zGt1oMe;fm*F5A=A%K@ z&rb)vY5sX&-a;iWAN7W3{c1vkjOt8%=Z>@9csM={4Q4Lm=fKz%?7NTo=mWM1qwku^ zJ^s&mr-DO_20y#_XIa5a%umZX@~)IQ_VRvjD}CDrS&_4^+$yk#^)29v{@%vXeU zt59y%dy9P&E&Y~$`YQ4-OJ6%z$8@o%My2NwZ_bPq@T-NDd1J>=}?_jNw^G?^X>LBl(^wmwy zcv9}25zld(eGM4DQ^vh}oOhqS%lJN4eT+Zn?d+?bF8N=&u2LS?fO5-e7O-A-71d zEucp7p2er#fik`TWP^`M%OqP5F}!a-J=|kzDnS)blT2 H%M0)iF*j7r literal 0 HcmV?d00001 diff --git a/src/test/splitbindingarray/compile.sh b/src/test/splitbindingarray/compile.sh new file mode 100755 index 0000000..0a3a0df --- /dev/null +++ b/src/test/splitbindingarray/compile.sh @@ -0,0 +1,6 @@ +set -e + +glslc buffer_binding_array.frag -o buffer_binding_array.spv +glslc storage_binding_array.frag -o storage_binding_array.spv +glslc texture_binding_array.frag -o texture_binding_array.spv +glslc nested_texture_binding_array.frag -o nested_texture_binding_array.spv diff --git a/src/test/splitbindingarray/nested_texture_binding_array.frag b/src/test/splitbindingarray/nested_texture_binding_array.frag new file mode 100644 index 0000000..23929e0 --- /dev/null +++ b/src/test/splitbindingarray/nested_texture_binding_array.frag @@ -0,0 +1,25 @@ +#version 440 + +#define MAX_TEXTURES 8 + +layout(location = 0) out vec4 o_color; + +layout(set = 0, binding = 0) uniform texture2D u_textures[MAX_TEXTURES]; +layout(set = 0, binding = 1) uniform sampler u_sampler; + +void process(texture2D textures[MAX_TEXTURES]) { + for (int i = 0; i < MAX_TEXTURES; i++) { + o_color += texture(sampler2D(textures[i], u_sampler), vec2(0.0, 0.0)); + } +} + +void process2(texture2D textures[MAX_TEXTURES]) { + process(textures); +} + +void main() { + o_color = vec4(0.0); + process(u_textures); + process2(u_textures); +} + diff --git a/src/test/splitbindingarray/nested_texture_binding_array.spv b/src/test/splitbindingarray/nested_texture_binding_array.spv new file mode 100644 index 0000000000000000000000000000000000000000..82ce1cade3f463ad647d08ebafd1f56b74d6e412 GIT binary patch literal 1464 zcmZ9MOHY$g5QY!5JWJ?~u3nUCfMSBFCw2;*TQ9EJ;-4uc>8Y$&tk%G=6Dr+c!oz4J`Q zWXNSiIa6@*SoQR{e&)el^g0|disR*|7{U({@?k8LE7kJRQMvM_T5Gkco$i-LT5Z(N z(&|aQoz}YbPpQ0-K3@H-);K@uClp}x_bf2SBt2T~W-aY>*1B7pAND^Uh5~;R;NA3d z_q?5Ua6yK{^d)!eKRG)qF#A=0F3f?|v-5m*zh<@8Y&6?h{YT*E)z0y0tC6-dP9f}P z*k#u;)A}yAQN`H2BI-NFSdL*%;MdE8%=McBv3^qaSA%7NYzPR2IJ)iA{U{6ctnB{M+~gXo*S*b`Oo5XzlGAxl?7AdiL>tiud>hQoz0<@^02S z=pbqp@7nv@Q{UbEhO>Z`tNjUg z5xayco-boP*S_T3y{@3v_|DAk@lFqOzklEu-$botu71p2#W(jgx$g2l>b&A|obdrF zZtOlEqwew@IqtE9$`OALmSFwC5_fOrCD={W_x;x6Z1pg?@$JRDr+0Q%ZJhNvzO#NM o-+S+%Vtxnl9mV?J(K}z=k-Jm+cjRx6{cd36o_F!xr)ROoKX^uNoB#j- literal 0 HcmV?d00001 diff --git a/src/test/splitbindingarray/storage_binding_array.frag b/src/test/splitbindingarray/storage_binding_array.frag new file mode 100644 index 0000000..a87f6a6 --- /dev/null +++ b/src/test/splitbindingarray/storage_binding_array.frag @@ -0,0 +1,19 @@ +#version 440 + +#define MAX_THINGS 8 + +layout(location = 0) out vec4 o_color; + +layout(set = 0, binding = 0, std140) buffer Thing { + vec4 thing1; + float thing2; +} u_things[MAX_THINGS]; + +void main() { + o_color = vec4(0.0); + for (int i = 0; i < MAX_THINGS; i++) { + o_color += u_things[i].thing1; + o_color.x += u_things[i].thing2; + } +} + diff --git a/src/test/splitbindingarray/storage_binding_array.spv b/src/test/splitbindingarray/storage_binding_array.spv new file mode 100644 index 0000000000000000000000000000000000000000..8b7e303a8cecd51b167c09a6fd274132cfaee6ee GIT binary patch literal 1288 zcmYk6S!+{K6opTk!P;sEJ2}*P(>S-r0Tn?M`XJYbKp*P+MUv27h)HQu!8iR$zWK{U z1;Ou2?!kM?>fU?p>713c)w$M%5Gr9YTn^8|?5u`4m;l$9+O%`jX^$tp_VX7{tXK-w zX`waC)YP!z)5m;h;v!g~MvmclRVIe;gN9mI2x%uv4-eDMyR3V9nvEx42K{W15Bpg! zKkIiV`RBfM^QF3c*d3hr$^~^Q$~%qSJIcDF!RRd1*||iV19R7~A5QY&F}49??3u_t z{m=QPDTf$!es+k@vneySJ}vghds6l|OGVz7=C(|7BIjJW72pi}o4^y}-B}pcP)og< zOm(jQCSC)ZfGzsf)mRM@e$QjAm498t>LIsBZ0!Q@>{DgkB9JR%gW5}Q38+kMjc>+m zuDr9f%fJ)kO?>aL&TY4_JJhL5yr*-o6DPo9pYaV~-!}0!)*d^9(VC8!2G=;@7v?Phxi`zdsy``|B}znx$5bW{{>UWx?2tWKDp~HS+!L^eR=ix zO}(G5ecGS&UBRvakNI`1dF>nWYPkWN@ttjIceUWu^ba-0tH4?I>c`$&`1T&~iUivP zY9#NStn<#hz+8-5_`cyAJI&t)a@NK7$;CZ)@Z%nJy${?YujbaN;{#ysfVt|s58R{f zvF1_1yW5^Nkh3@TJ;slH-pANFHB?V^(8l}ukIDOeWu}_)CmrOxTl|dV%Ab*X{^i^9 G3j6~!_f*aR literal 0 HcmV?d00001 diff --git a/src/test/splitbindingarray/texture_binding_array.frag b/src/test/splitbindingarray/texture_binding_array.frag new file mode 100644 index 0000000..bfa7136 --- /dev/null +++ b/src/test/splitbindingarray/texture_binding_array.frag @@ -0,0 +1,16 @@ +#version 440 + +#define MAX_TEXTURES 8 + +layout(location = 0) out vec4 o_color; + +layout(set = 0, binding = 0) uniform texture2D u_textures[MAX_TEXTURES]; +layout(set = 0, binding = 1) uniform sampler u_sampler; + +void main() { + o_color = vec4(0.0); + for (int i = 0; i < MAX_TEXTURES; i++) { + o_color += texture(sampler2D(u_textures[i], u_sampler), vec2(0.0, 0.0)); + } +} + diff --git a/src/test/splitbindingarray/texture_binding_array.spv b/src/test/splitbindingarray/texture_binding_array.spv new file mode 100644 index 0000000000000000000000000000000000000000..c330ca736b8310a2fd441f8f6c63fa6df6a03b85 GIT binary patch literal 1200 zcmYk6TWb?h6opTlrqkMhkA72V#F06#r@HAZ5W|#*FV2eeJyYIW5Y2NQVd;Ubm zT4)qRIqPs1i2C_iHgw}Mz5z#!=ByfY4B-a_3t>6L-6TFejl1uX-gul$^Dl!m8DzsW z>1UI)muH_+c}q23HtY>%{W_ruqkg8j=Z=!zXfT?DCTFgZXLysp8^l?Xr=RoLB%NMh zcM5EJc0L}Y6V`FrL#}4-x>aQV^INst9`37!eDi95i>aNvRVWaO^)Bm0un6CSuRi9^ z67heDsJh~>%G|lcPRQjh<60Y*ycJxm?mKX=fJL}>v-c`dzPP=N>$q0syR5y#Hn-g) z?xX5H(R1xnw<`Cn12&>^(_ZF>xV?_ZkBIgW^Dftknz(bCSGv0&k!#i*aF=7E_-#SV z+&XhL*A7wX9?=@LeL(cyzCoLB!2BUz=~T`97;FxI0Y|QHp{e;VkS5Wy)X#594d1Zq zyQsZuakcyne}ib9@h5dxiCehl{x;El<7@VPhaKD=-#Ja)ua%hZ@du9iCT=flU01a? zSzG&-+KAY~)lyuJdiYI>o2%VjT-`oUqh|MUIpQC|obdz~S2OFx+PHInD>;u!Om3x# lm~%R_cQsab?XdQ)U#WL)e^A7HgGxuSx{l8Lmlt}8{{u+fP-_4H literal 0 HcmV?d00001 From 078e168cb66d40a504896436c8c84d78db19b170 Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 13 May 2026 19:29:42 -0700 Subject: [PATCH 02/16] WIP initial direct usage implementation --- src/bin/spv_webgpu_transform.rs | 5 +- src/correction.rs | 14 +- src/lib.rs | 2 + src/mirrorpatch.rs | 2 +- src/pruneunuseddref.rs | 1 - src/splitbindingarray.rs | 322 +++++++++++++++++++++++ src/splitbindingarray/select_template.rs | 130 +++++++++ src/splitcombined.rs | 3 +- src/splitdref.rs | 2 +- src/spv.rs | 3 + src/storagecubepatch.rs | 2 +- src/util/decorate.rs | 30 ++- 12 files changed, 491 insertions(+), 25 deletions(-) create mode 100644 src/splitbindingarray.rs create mode 100644 src/splitbindingarray/select_template.rs diff --git a/src/bin/spv_webgpu_transform.rs b/src/bin/spv_webgpu_transform.rs index 6183d1d..0f31ba6 100644 --- a/src/bin/spv_webgpu_transform.rs +++ b/src/bin/spv_webgpu_transform.rs @@ -5,7 +5,7 @@ fn main() { if args.len() != 4 { eprintln!( - "Usage: spv_webgpu_transform " + "Usage: spv_webgpu_transform " ); process::exit(1); } @@ -32,6 +32,9 @@ fn main() { } "pruneunuseddref" => spirv_webgpu_transform::pruneunuseddref(&spv).unwrap(), "immediates" => spirv_webgpu_transform::immediatespatch(&spv).unwrap(), + "bindingarray" => { + spirv_webgpu_transform::splitbindingarray(&spv, &mut out_correction_map).unwrap() + } mode => { eprintln!("unknown mode {:?}", mode); process::exit(1) diff --git a/src/correction.rs b/src/correction.rs index fb0723d..6dc91c2 100644 --- a/src/correction.rs +++ b/src/correction.rs @@ -3,18 +3,22 @@ use super::*; // Q: Hey what happens when you stack corrections? // A: I don't want to think about it... I will start thinking after a refactor... -#[repr(u16)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CorrectionType { /// A combined image sampler has been split, a new `sampler` object should be inserted. - SplitCombined = 0, + SplitCombined, /// A mixed depth texture / sampler has been duplicated, insert the same object again with a `Regular` bind type. - SplitDrefRegular = 1, + SplitDrefRegular, /// A mixed depth texture / sampler has been duplicated, insert the same object again with a /// `Comparison` bind type. - SplitDrefComparison = 2, + SplitDrefComparison, /// A storage cube texture has been converted into a storage texture 2D array, change the dimension. - ConvertStorageCube = 3, + ConvertStorageCube, + /// A binding array has been split into N new variables. + /// In ffi form, this is represented by + /// `SPLIT_BINDING_ARRAY_BASE <= v < SPLIT_BINDING_ARRAY_MAX` + /// where N = `v - SPLIT_BINDING_ARRAY_BASE`. + SplitBindingArray(u32), } #[derive(Debug, Clone, Default, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index 2bf78a8..167c823 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,7 @@ mod immediatespatch; mod isnanisinfpatch; mod mirrorpatch; mod pruneunuseddref; +mod splitbindingarray; mod splitcombined; mod splitdref; mod spv; @@ -51,6 +52,7 @@ pub use correction::*; pub use immediatespatch::*; pub use isnanisinfpatch::*; pub use mirrorpatch::*; +pub use splitbindingarray::*; pub use pruneunuseddref::*; pub use splitcombined::*; pub use splitdref::*; diff --git a/src/mirrorpatch.rs b/src/mirrorpatch.rs index ae073e4..3f063c9 100644 --- a/src/mirrorpatch.rs +++ b/src/mirrorpatch.rs @@ -214,7 +214,7 @@ fn patch_spv_decorations( // Convert into affected decoration Ok(AffectedDecoration { original_res_id: original_variable_id, - new_res_id, + new_res_ids: vec![new_res_id], correction_type, }) }) diff --git a/src/pruneunuseddref.rs b/src/pruneunuseddref.rs index 6c2665c..422c53c 100644 --- a/src/pruneunuseddref.rs +++ b/src/pruneunuseddref.rs @@ -186,7 +186,6 @@ pub fn pruneunuseddref(in_spv: &[u32]) -> Result, ()> { new_spv[spv_idx..spv_idx + word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); } - prune_noops(&mut new_spv); // 10. Write New Header and New Code diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs new file mode 100644 index 0000000..62e00ea --- /dev/null +++ b/src/splitbindingarray.rs @@ -0,0 +1,322 @@ +use super::*; + +fn inc(ib: &mut u32) -> u32 { + *ib += 1; + *ib - 1 +} + +mod select_template; + +use select_template::*; + +/// Perform the operation on a `Vec`. +/// Use [u8_slice_to_u32_vec] to convert a `&[u8]` into a `Vec` +/// Either update the existing `corrections` or create a new one. +pub fn splitbindingarray( + in_spv: &[u32], + corrections: &mut Option, +) -> Result, ()> { + let spv = in_spv.to_owned(); + + let mut instruction_bound = spv[SPV_HEADER_INSTRUCTION_BOUND_OFFSET]; + let magic_number = spv[SPV_HEADER_MAGIC_NUM_OFFSET]; + + let spv_header = spv[0..SPV_HEADER_LENGTH].to_owned(); + + assert_eq!(magic_number, SPV_HEADER_MAGIC); + + let mut instruction_inserts = vec![]; + let word_inserts = vec![]; + + let spv = spv.into_iter().skip(SPV_HEADER_LENGTH).collect::>(); + let mut new_spv = spv.clone(); + + let mut op_type_int_idxs = vec![]; + let mut op_type_array_idxs = vec![]; + let mut op_type_pointer_idxs = vec![]; + let mut op_variable_idxs = vec![]; + let mut op_access_chain_idxs = vec![]; + let mut op_type_function_idxs = vec![]; + let mut op_function_parameter_idxs = vec![]; + let mut op_function_call_idxs = vec![]; + let mut op_function_end_idxs = vec![]; + let mut op_decorate_idxs = vec![]; + let mut op_name_idxs = vec![]; + + // 1. Find locations instructions we need + let mut spv_idx = 0; + while spv_idx < spv.len() { + let op = spv[spv_idx]; + let word_count = hiword(op); + let instruction = loword(op); + + match instruction { + SPV_INSTRUCTION_OP_TYPE_INT => op_type_int_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_TYPE_ARRAY => op_type_array_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_TYPE_POINTER => op_type_pointer_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_VARIABLE => op_variable_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_ACCESS_CHAIN => op_access_chain_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_TYPE_FUNCTION => op_type_function_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_FUNCTION_PARAMETER => op_function_parameter_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_FUNCTION_CALL => op_function_call_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_FUNCTION_END => op_function_end_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_DECORATE => op_decorate_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_NAME => op_name_idxs.push(spv_idx), + + _ => {} + } + + spv_idx += word_count as usize; + } + + // TODO: Implement for nested arrays. + for ta_idx in op_type_array_idxs.iter() { + let ta_underlying_id = spv[ta_idx + 2]; + for ta_jdx in op_type_array_idxs.iter() { + if spv[ta_jdx + 2] == ta_underlying_id && ta_idx != ta_jdx { + unimplemented!("How dare you use nested arrays! (Unimplemented)"); + } + } + } + + // 2. OpTypeArray -> OpTypePointer -> OpVariable + let array_tp_ta_idxs = op_type_pointer_idxs + .iter() + .filter_map(|&tp_idx| { + let tp_storage_class = spv[tp_idx + 2]; + let tp_underlying_id = spv[tp_idx + 3]; + + if tp_storage_class != SPV_STORAGE_CLASS_UNIFORM_CONSTANT + && tp_storage_class != SPV_STORAGE_CLASS_UNIFORM + { + return None; + } + + op_type_array_idxs + .iter() + .find(|&ta_idx| { + let ta_res_id = spv[ta_idx + 1]; + + ta_res_id == tp_underlying_id + }) + .map(|&ta_idx| (tp_idx, ta_idx)) + }) + .collect::>(); + + let array_v_ta_idxs = op_variable_idxs + .iter() + .filter_map(|&v_idx| { + let variable_type_id = spv[v_idx + 1]; + array_tp_ta_idxs + .iter() + .find(|&(tp_idx, _)| { + let tp_res_id = spv[tp_idx + 1]; + tp_res_id == variable_type_id + }) + .map(|&(_, ta_idx)| (v_idx, ta_idx)) + }) + .collect::>(); + + // 3. Unroll array variables + let types_header_position = last_of_indices!(op_type_int_idxs, op_type_pointer_idxs); + let mut types_header_insert = InstructionInsert { + previous_spv_idx: types_header_position.unwrap(), + instruction: vec![], + }; + let mut new_variables_map = HashMap::new(); + let mut affected_decorations = vec![]; + + for &(v_idx, ta_idx) in array_v_ta_idxs.iter() { + new_spv[v_idx..v_idx + hiword(spv[v_idx]) as usize] + .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + + let mut new_instruction = vec![]; + + let underlying_type_id = spv[ta_idx + 2]; + let type_pointer_id = ensure_type_pointer( + &spv, + &op_type_pointer_idxs, + &mut instruction_bound, + &mut new_instruction, + SPV_STORAGE_CLASS_UNIFORM_CONSTANT, + underlying_type_id, + ); + let length = spv[ta_idx + 3]; + let base_id = instruction_bound; + instruction_bound += length; + + for i in 0..length { + new_instruction.append(&mut vec![ + encode_word(4, SPV_INSTRUCTION_OP_VARIABLE), + type_pointer_id, + base_id + i, + SPV_STORAGE_CLASS_UNIFORM_CONSTANT, + ]); + } + // Ordering issues with this, let's keep it after all other type pointers. + // + // instruction_inserts.push(InstructionInsert { + // previous_spv_idx: v_idx, + // instruction: new_instruction, + // }); + types_header_insert.instruction.append(&mut new_instruction); + new_variables_map.insert(v_idx, base_id); + + let old_result_id = spv[v_idx + 2]; + let new_ids = (base_id..base_id + length).collect::>(); + affected_decorations.push(AffectedDecoration { + original_res_id: old_result_id, + new_res_ids: new_ids, + correction_type: CorrectionType::SplitBindingArray(length), + }); + } + + // 4. Create selection functions + // Ordering issues, so let's just put the function body as the very end. + let fn_body_header_position = last_of_indices!( + // op_type_int_idxs, + // op_type_pointer_idxs, + // op_variable_idxs, + op_function_end_idxs + ); + let mut fn_body_header_insert = InstructionInsert { + previous_spv_idx: fn_body_header_position.unwrap(), + instruction: vec![], + }; + let uint32_id = ensure_type_int( + &spv, + &op_type_int_idxs, + &mut instruction_bound, + &mut types_header_insert.instruction, + 32, + SPV_SIGNEDNESS_UNSIGNED, + ); + + let mut selection_map = HashMap::new(); + for (_, ta) in array_tp_ta_idxs.iter() { + let underlying_type_id = spv[ta + 2]; + let length = spv[ta + 3]; + + if let std::collections::hash_map::Entry::Vacant(e) = + selection_map.entry((underlying_type_id, length)) + { + let type_pointer_id = ensure_type_pointer( + &spv, + &op_type_pointer_idxs, + &mut instruction_bound, + &mut types_header_insert.instruction, + SPV_STORAGE_CLASS_UNIFORM_CONSTANT, + underlying_type_id, + ); + println!("GG {:?}", type_pointer_id); + + let inputs = SelectTemplateFunctionInputs { + uint32_id, + item_type_id: type_pointer_id, + }; + let (fn_type_id, mut fn_type_spv) = + select_template_type_spv(&mut instruction_bound, inputs, length as usize); + let (fn_id, mut fn_spv) = + select_template_spv(&mut instruction_bound, inputs, fn_type_id, length as usize); + types_header_insert.instruction.append(&mut fn_type_spv); + fn_body_header_insert.instruction.append(&mut fn_spv); + e.insert(fn_id); + } + } + + // 5. Replace OpAccessChain with selection function + for (ac_idx, v_idx, ta_idx) in op_access_chain_idxs.iter().filter_map(|&ac_idx| { + let base_id = spv[ac_idx + 3]; + array_v_ta_idxs + .iter() + .find(|&(v_idx, _)| { + let result_id = spv[*v_idx + 2]; + result_id == base_id + }) + .map(|(v_idx, ta_idx)| (ac_idx, v_idx, ta_idx)) + }) { + new_spv[ac_idx..ac_idx + hiword(spv[ac_idx]) as usize] + .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + + let old_result_type_id = spv[ac_idx + 1]; + let old_result_id = spv[ac_idx + 2]; + + let underlying_type_id = spv[ta_idx + 2]; + let length = spv[ta_idx + 3]; + let fn_id = selection_map[&(underlying_type_id, length)]; + + let base_id = new_variables_map[v_idx]; + + let mut call = vec![ + encode_word(5 + length as u16, SPV_INSTRUCTION_OP_FUNCTION_CALL), + old_result_type_id, + old_result_id, + fn_id, + length, + ]; + call.append(&mut (base_id..base_id + length).collect::>()); + instruction_inserts.push(InstructionInsert { + previous_spv_idx: ac_idx, + instruction: call, + }); + } + + // 6. Find OpDecorate / OpName to OpVariable + let unused_decorate_idxs = op_decorate_idxs + .iter() + .filter(|&idx| { + let target = spv[idx + 1]; + new_variables_map.iter().any(|(v_idx, _)| { + let result_id = spv[v_idx + 2]; + target == result_id + }) + }) + .copied() + .collect::>(); + let unused_name_idxs = op_name_idxs + .iter() + .filter(|&idx| { + let target = spv[idx + 1]; + new_variables_map.iter().any(|(v_idx, _)| { + let result_id = spv[v_idx + 2]; + target == result_id + }) + }) + .copied() + .collect::>(); + + // 7. Remove Instructions that have been Whited Out. + for &spv_idx in unused_decorate_idxs.iter().chain(unused_name_idxs.iter()) { + let op = spv[spv_idx]; + let word_count = hiword(op) as usize; + + new_spv[spv_idx..spv_idx + word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + } + + // 8. OpDecorate + let DecorateOut { + descriptor_sets_to_correct, + } = util::decorate(DecorateIn { + spv: &spv, + instruction_inserts: &mut instruction_inserts, + first_op_deocrate_idx: op_decorate_idxs.first().copied(), + op_decorate_idxs: &op_decorate_idxs, + affected_decorations: &affected_decorations, + corrections, + }); + + // 9. Insert New Instructions + instruction_inserts.insert(0, types_header_insert); + instruction_inserts.insert(0, fn_body_header_insert); + insert_new_instructions(&spv, &mut new_spv, &word_inserts, &instruction_inserts); + + // 10. Correct OpDecorate Bindings + util::correct_decorate(CorrectDecorateIn { + new_spv: &mut new_spv, + descriptor_sets_to_correct, + }); + prune_noops(&mut new_spv); + + // 11. Write New Header and New Code + Ok(fuse_final(spv_header, new_spv, instruction_bound)) +} diff --git a/src/splitbindingarray/select_template.rs b/src/splitbindingarray/select_template.rs new file mode 100644 index 0000000..c83e614 --- /dev/null +++ b/src/splitbindingarray/select_template.rs @@ -0,0 +1,130 @@ +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct SelectTemplateFunctionInputs { + pub uint32_id: u32, + pub item_type_id: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct SelectFunctionType(pub u32); + +pub(super) fn select_template_type_spv( + ib: &mut u32, + inputs: SelectTemplateFunctionInputs, + length: usize, +) -> (SelectFunctionType, Vec) { + // %_select_type_fn = OpTypeFunction %item_type %uint_32 %item_type [%item_type ..] + let SelectTemplateFunctionInputs { + uint32_id, + item_type_id, + } = inputs; + let fn_type_id = inc(ib); + let mut spv = vec![]; + + spv.extend_from_slice(&[ + encode_word(length as u16 + 4, SPV_INSTRUCTION_OP_TYPE_FUNCTION), + fn_type_id, + item_type_id, + uint32_id, + ]); + for _ in 0..length { + spv.push(item_type_id); + } + + (SelectFunctionType(fn_type_id), spv) +} + +pub(super) fn select_template_spv( + ib: &mut u32, + inputs: SelectTemplateFunctionInputs, + function_type: SelectFunctionType, + length: usize, +) -> (u32, Vec) { + // %_select_fn = OpFunction %item_type Inline %_select_type_fn + // %idx = OpFunctionParameter %uint_32 + // %item_0 = OpFunctionParameter %item_type + // ... + // %item_N = OpFunctionParameter %item_type + // %1 = OpLabel + // OpSelectionMerge %merge None + // OpSwitch %idx %merge 0 %case_0 1 %case_1 ... N %case_N + // %case_0 = OpLabel + // OpReturnValue %item_0 + // %case_1 = OpLabel + // OpReturnValue %item_1 + // ... + // %case_N = OpLabel + // OpReturnValue %item_N + // %merge = OpLabel + // OpReturnValue %item_0 + // OpFunctionEnd + // + + let SelectTemplateFunctionInputs { + uint32_id, + item_type_id, + } = inputs; + + let SelectFunctionType(fn_type_id) = function_type; + let fn_id = inc(ib); + let idx_param = inc(ib); + let item_params = (0..length).map(|_| inc(ib)).collect::>(); + let entry_label = inc(ib); + let case_labels = (0..length).map(|_| inc(ib)).collect::>(); + let merge_label = inc(ib); + + let mut spv = vec![]; + + spv.extend_from_slice(&[ + encode_word(5, SPV_INSTRUCTION_OP_FUNCTION), + item_type_id, + fn_id, + SPV_FUNCTION_CONTROL_INLINE, + fn_type_id, + encode_word(3, SPV_INSTRUCTION_OP_FUNCTION_PARAMETER), + uint32_id, + idx_param, + ]); + for &item_param in &item_params { + spv.extend_from_slice(&[ + encode_word(3, SPV_INSTRUCTION_OP_FUNCTION_PARAMETER), + item_type_id, + item_param, + ]); + } + + spv.extend_from_slice(&[ + encode_word(2, SPV_INSTRUCTION_OP_LABEL), + entry_label, + encode_word(3, SPV_INSTRUCTION_OP_SELECTION_MERGE), + merge_label, + SPV_SELECTION_CONTROL_NONE, + encode_word(3 + 2 * length as u16, SPV_INSTRUCTION_OP_SWITCH), + idx_param, + merge_label, + ]); + for (i, &case_label) in case_labels.iter().enumerate() { + spv.push(i as u32); + spv.push(case_label); + } + + for (case_label, item_param) in case_labels.iter().zip(item_params.iter()) { + spv.extend_from_slice(&[ + encode_word(2, SPV_INSTRUCTION_OP_LABEL), + *case_label, + encode_word(2, SPV_INSTRUCTION_OP_RETURN_VALUE), + *item_param, + ]); + } + + spv.extend_from_slice(&[ + encode_word(2, SPV_INSTRUCTION_OP_LABEL), + merge_label, + encode_word(2, SPV_INSTRUCTION_OP_RETURN_VALUE), + item_params[0], + encode_word(1, SPV_INSTRUCTION_OP_FUNCTION_END), + ]); + + (fn_id, spv) +} diff --git a/src/splitcombined.rs b/src/splitcombined.rs index 789be03..a27edd9 100644 --- a/src/splitcombined.rs +++ b/src/splitcombined.rs @@ -70,6 +70,7 @@ pub fn combimgsampsplitter( } SPV_INSTRUCTION_OP_TYPE_SAMPLED_IMAGE => op_type_sampled_image_idxs.push(spv_idx), SPV_INSTRUCTION_OP_TYPE_POINTER => { + // This should probably go elsewhere. #[allow(clippy::collapsible_match)] if spv[spv_idx + 2] == SPV_STORAGE_CLASS_UNIFORM_CONSTANT { op_type_pointer_idxs.push(spv_idx); @@ -199,7 +200,7 @@ pub fn combimgsampsplitter( }| { AffectedDecoration { original_res_id: *v_res_id, - new_res_id: *new_sampler_v_res_id, + new_res_ids: vec![*new_sampler_v_res_id], correction_type: CorrectionType::SplitCombined, } }, diff --git a/src/splitdref.rs b/src/splitdref.rs index cdba030..b42fad1 100644 --- a/src/splitdref.rs +++ b/src/splitdref.rs @@ -552,7 +552,7 @@ pub fn drefsplitter( affected_variables.push(AffectedDecoration { original_res_id: spv[variable_idx + 2], - new_res_id: new_variable_id, + new_res_ids: vec![new_variable_id], correction_type: match complement_ty { OperationVariant::Regular => CorrectionType::SplitDrefRegular, OperationVariant::Dref => CorrectionType::SplitDrefComparison, diff --git a/src/spv.rs b/src/spv.rs index cb17b41..16a4d6a 100644 --- a/src/spv.rs +++ b/src/spv.rs @@ -75,6 +75,9 @@ pub const SPV_INSTRUCTION_OP_PHI: u16 = 245; pub const SPV_INSTRUCTION_OP_SELECTION_MERGE: u16 = 247; pub const SPV_INSTRUCTION_OP_BRANCH: u16 = 249; pub const SPV_INSTRUCTION_OP_BRANCH_CONDITIONAL: u16 = 250; +pub const SPV_INSTRUCTION_OP_SWITCH: u16 = 251; + +pub const SPV_SELECTION_CONTROL_NONE: u32 = 0; pub const SPV_STORAGE_CLASS_UNIFORM_CONSTANT: u32 = 0; pub const SPV_STORAGE_CLASS_UNIFORM: u32 = 2; diff --git a/src/storagecubepatch.rs b/src/storagecubepatch.rs index 99f1608..76c10bf 100644 --- a/src/storagecubepatch.rs +++ b/src/storagecubepatch.rs @@ -346,7 +346,7 @@ pub fn storagecubepatch( .iter() .map(|id| AffectedDecoration { original_res_id: *id, - new_res_id: *id, + new_res_ids: vec![*id], correction_type: CorrectionType::ConvertStorageCube, }) .collect::>(), diff --git a/src/util/decorate.rs b/src/util/decorate.rs index e00aa11..7f2b459 100644 --- a/src/util/decorate.rs +++ b/src/util/decorate.rs @@ -1,9 +1,9 @@ use super::*; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct AffectedDecoration { pub original_res_id: u32, - pub new_res_id: u32, + pub new_res_ids: Vec, pub correction_type: CorrectionType, } @@ -62,21 +62,23 @@ pub fn decorate(d_in: DecorateIn) -> DecorateOut { affected_variables.iter().for_each( |AffectedDecoration { original_res_id, - new_res_id, + new_res_ids, correction_type, }| { if *original_res_id == target_id { - if decoration_id == SPV_DECORATION_BINDING { - new_variable_id_to_decorations - .entry((new_res_id, correction_type)) - .or_insert((None, None)) - .0 = Some((d_idx, decoration_value)); - } else if decoration_id == SPV_DECORATION_DESCRIPTOR_SET { - new_variable_id_to_decorations - .entry((new_res_id, correction_type)) - .or_insert((None, None)) - .1 = Some((d_idx, decoration_value)); - descriptor_sets_to_correct.insert(decoration_value); + for new_res_id in new_res_ids { + if decoration_id == SPV_DECORATION_BINDING { + new_variable_id_to_decorations + .entry((new_res_id, correction_type)) + .or_insert((None, None)) + .0 = Some((d_idx, decoration_value)); + } else if decoration_id == SPV_DECORATION_DESCRIPTOR_SET { + new_variable_id_to_decorations + .entry((new_res_id, correction_type)) + .or_insert((None, None)) + .1 = Some((d_idx, decoration_value)); + descriptor_sets_to_correct.insert(decoration_value); + } } } }, From 2b72cab13158f5a587ca8cf888c16e162e6ed33f Mon Sep 17 00:00:00 2001 From: davnotdev Date: Mon, 18 May 2026 09:27:09 -0700 Subject: [PATCH 03/16] WIP --- src/lib.rs | 2 +- src/splitbindingarray.rs | 148 ++++++------- src/splitbindingarray/select_template.rs | 194 +++++++++--------- src/spv.rs | 2 + .../buffer_binding_array.frag | 2 + .../buffer_binding_array.spv | Bin 1288 -> 1356 bytes src/test/splitbindingarray/compile.sh | 8 +- src/test/splitcombined/compile.sh | 8 +- src/test/splitdref/compile.sh | 22 +- src/test/storagecubepatch/compile.sh | 6 +- 10 files changed, 191 insertions(+), 201 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 167c823..47d5eb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,8 +52,8 @@ pub use correction::*; pub use immediatespatch::*; pub use isnanisinfpatch::*; pub use mirrorpatch::*; -pub use splitbindingarray::*; pub use pruneunuseddref::*; +pub use splitbindingarray::*; pub use splitcombined::*; pub use splitdref::*; pub use storagecubepatch::*; diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index 62e00ea..184b983 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -36,6 +36,10 @@ pub fn splitbindingarray( let mut op_type_pointer_idxs = vec![]; let mut op_variable_idxs = vec![]; let mut op_access_chain_idxs = vec![]; + let mut op_in_bounds_access_chain_idxs = vec![]; + let mut op_load_idxs = vec![]; + let mut op_store_idxs = vec![]; + let mut op_copy_memory_idxs = vec![]; let mut op_type_function_idxs = vec![]; let mut op_function_parameter_idxs = vec![]; let mut op_function_call_idxs = vec![]; @@ -56,6 +60,12 @@ pub fn splitbindingarray( SPV_INSTRUCTION_OP_TYPE_POINTER => op_type_pointer_idxs.push(spv_idx), SPV_INSTRUCTION_OP_VARIABLE => op_variable_idxs.push(spv_idx), SPV_INSTRUCTION_OP_ACCESS_CHAIN => op_access_chain_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN => { + op_in_bounds_access_chain_idxs.push(spv_idx) + } + SPV_INSTRUCTION_OP_LOAD => op_load_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_STORE => op_store_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_COPY_MEMORY => op_copy_memory_idxs.push(spv_idx), SPV_INSTRUCTION_OP_TYPE_FUNCTION => op_type_function_idxs.push(spv_idx), SPV_INSTRUCTION_OP_FUNCTION_PARAMETER => op_function_parameter_idxs.push(spv_idx), SPV_INSTRUCTION_OP_FUNCTION_CALL => op_function_call_idxs.push(spv_idx), @@ -171,94 +181,73 @@ pub fn splitbindingarray( }); } - // 4. Create selection functions - // Ordering issues, so let's just put the function body as the very end. - let fn_body_header_position = last_of_indices!( - // op_type_int_idxs, - // op_type_pointer_idxs, - // op_variable_idxs, - op_function_end_idxs - ); - let mut fn_body_header_insert = InstructionInsert { - previous_spv_idx: fn_body_header_position.unwrap(), - instruction: vec![], - }; - let uint32_id = ensure_type_int( - &spv, - &op_type_int_idxs, - &mut instruction_bound, - &mut types_header_insert.instruction, - 32, - SPV_SIGNEDNESS_UNSIGNED, - ); - - let mut selection_map = HashMap::new(); - for (_, ta) in array_tp_ta_idxs.iter() { - let underlying_type_id = spv[ta + 2]; - let length = spv[ta + 3]; - - if let std::collections::hash_map::Entry::Vacant(e) = - selection_map.entry((underlying_type_id, length)) - { - let type_pointer_id = ensure_type_pointer( - &spv, - &op_type_pointer_idxs, - &mut instruction_bound, - &mut types_header_insert.instruction, - SPV_STORAGE_CLASS_UNIFORM_CONSTANT, - underlying_type_id, - ); - println!("GG {:?}", type_pointer_id); - - let inputs = SelectTemplateFunctionInputs { - uint32_id, - item_type_id: type_pointer_id, - }; - let (fn_type_id, mut fn_type_spv) = - select_template_type_spv(&mut instruction_bound, inputs, length as usize); - let (fn_id, mut fn_spv) = - select_template_spv(&mut instruction_bound, inputs, fn_type_id, length as usize); - types_header_insert.instruction.append(&mut fn_type_spv); - fn_body_header_insert.instruction.append(&mut fn_spv); - e.insert(fn_id); - } - } - // 5. Replace OpAccessChain with selection function - for (ac_idx, v_idx, ta_idx) in op_access_chain_idxs.iter().filter_map(|&ac_idx| { - let base_id = spv[ac_idx + 3]; - array_v_ta_idxs - .iter() - .find(|&(v_idx, _)| { - let result_id = spv[*v_idx + 2]; - result_id == base_id - }) - .map(|(v_idx, ta_idx)| (ac_idx, v_idx, ta_idx)) - }) { + for (ac_idx, v_idx, ta_idx) in op_access_chain_idxs + .iter() + .chain(op_in_bounds_access_chain_idxs.iter()) + .filter_map(|&ac_idx| { + let base_id = spv[ac_idx + 3]; + array_v_ta_idxs + .iter() + .find(|&(v_idx, _)| { + let result_id = spv[*v_idx + 2]; + result_id == base_id + }) + .map(|(v_idx, ta_idx)| (ac_idx, v_idx, ta_idx)) + }) + { new_spv[ac_idx..ac_idx + hiword(spv[ac_idx]) as usize] .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); - let old_result_type_id = spv[ac_idx + 1]; let old_result_id = spv[ac_idx + 2]; - - let underlying_type_id = spv[ta_idx + 2]; + let index_0_id = spv[ac_idx + 4]; let length = spv[ta_idx + 3]; - let fn_id = selection_map[&(underlying_type_id, length)]; let base_id = new_variables_map[v_idx]; - let mut call = vec![ - encode_word(5 + length as u16, SPV_INSTRUCTION_OP_FUNCTION_CALL), - old_result_type_id, - old_result_id, - fn_id, - length, - ]; - call.append(&mut (base_id..base_id + length).collect::>()); - instruction_inserts.push(InstructionInsert { - previous_spv_idx: ac_idx, - instruction: call, - }); + // Find all dependent operations afterwards and replace each instruction with index switch + for &spv_idx in op_load_idxs + .iter() + .chain(op_store_idxs.iter()) + .chain(op_access_chain_idxs.iter()) + .chain(op_in_bounds_access_chain_idxs.iter()) + .chain(op_copy_memory_idxs.iter()) + { + let word_count = hiword(spv[spv_idx]) as usize; + let instruction = loword(spv[spv_idx]); + + let source_offset = match instruction { + SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_COPY_MEMORY => 2, + SPV_INSTRUCTION_OP_LOAD + | SPV_INSTRUCTION_OP_ACCESS_CHAIN + | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN => 3, + _ => unreachable!("Unexpected instruction {} while matching", instruction), + }; + + let source_id = spv[spv_idx + source_offset]; + if source_id == old_result_id && ac_idx != spv_idx { + if instruction == SPV_INSTRUCTION_OP_ACCESS_CHAIN + || instruction == SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN + { + unimplemented!( + "Nested OpAccessChain / OpInBoundsAccessChain on binding array (Unimplemented)" + ); + } + + new_spv[spv_idx..spv_idx + word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + let switch = select_template_spv( + &mut instruction_bound, + index_0_id, + base_id, + &spv[spv_idx..spv_idx + word_count], + length as usize, + ); + instruction_inserts.push(InstructionInsert { + previous_spv_idx: spv_idx, + instruction: switch, + }); + } + } } // 6. Find OpDecorate / OpName to OpVariable @@ -307,7 +296,6 @@ pub fn splitbindingarray( // 9. Insert New Instructions instruction_inserts.insert(0, types_header_insert); - instruction_inserts.insert(0, fn_body_header_insert); insert_new_instructions(&spv, &mut new_spv, &word_inserts, &instruction_inserts); // 10. Correct OpDecorate Bindings diff --git a/src/splitbindingarray/select_template.rs b/src/splitbindingarray/select_template.rs index c83e614..03ad125 100644 --- a/src/splitbindingarray/select_template.rs +++ b/src/splitbindingarray/select_template.rs @@ -1,130 +1,128 @@ use super::*; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(super) struct SelectTemplateFunctionInputs { - pub uint32_id: u32, - pub item_type_id: u32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) struct SelectFunctionType(pub u32); - -pub(super) fn select_template_type_spv( - ib: &mut u32, - inputs: SelectTemplateFunctionInputs, - length: usize, -) -> (SelectFunctionType, Vec) { - // %_select_type_fn = OpTypeFunction %item_type %uint_32 %item_type [%item_type ..] - let SelectTemplateFunctionInputs { - uint32_id, - item_type_id, - } = inputs; - let fn_type_id = inc(ib); - let mut spv = vec![]; - - spv.extend_from_slice(&[ - encode_word(length as u16 + 4, SPV_INSTRUCTION_OP_TYPE_FUNCTION), - fn_type_id, - item_type_id, - uint32_id, - ]); - for _ in 0..length { - spv.push(item_type_id); - } - - (SelectFunctionType(fn_type_id), spv) -} - +// Take the following instructions: +// OpLoad, OpStore, OpAccessChain, OpInBoundsAccessChain, OpCopyMemory +// (i) (i) (i) +// +// The instruction's source, `[idx+2]` is replaced with `%base_id+N` +// and duplicated for each case of the index, see template below. +// pub(super) fn select_template_spv( ib: &mut u32, - inputs: SelectTemplateFunctionInputs, - function_type: SelectFunctionType, + index_id: u32, + base_id: u32, + original_instruction: &[u32], length: usize, -) -> (u32, Vec) { - // %_select_fn = OpFunction %item_type Inline %_select_type_fn - // %idx = OpFunctionParameter %uint_32 - // %item_0 = OpFunctionParameter %item_type - // ... - // %item_N = OpFunctionParameter %item_type - // %1 = OpLabel - // OpSelectionMerge %merge None - // OpSwitch %idx %merge 0 %case_0 1 %case_1 ... N %case_N - // %case_0 = OpLabel - // OpReturnValue %item_0 - // %case_1 = OpLabel - // OpReturnValue %item_1 - // ... - // %case_N = OpLabel - // OpReturnValue %item_N - // %merge = OpLabel - // OpReturnValue %item_0 - // OpFunctionEnd +) -> Vec { + // + // TODO: You can probably decrease the instruction count with OpPhi or OpSelect. + // + // OpSelectionMerge %merge None + // OpSwitch %index_id %default %merge 0 %case_0 1 %case_1 ... N %case_N + // %case_0 = OpLabel + // %temp_0 = {INSTRUCTION}(%base_id+0) + // OpBranch %merge + // %case_1 = OpLabel + // %temp_1 = {INSTRUCTION}(%base_id+1) + // OpBranch %merge + // ... + // %case_N = OpLabel + // %temp_N = {INSTRUCTION}(%base_id+N) + // OpBranch %merge + // %default = OpLabel + // {INSTRUCTION}(%base_id+0) + // %merge = OpLabel + // + // ; Only if there will be a result value. + // %target_id = OpPhi %underlying_type_id %temp_0 %case_0 %temp_1 %case_1 ... %temp_N %case_N + // // - let SelectTemplateFunctionInputs { - uint32_id, - item_type_id, - } = inputs; + let returns_result = matches!( + loword(original_instruction[0]), + SPV_INSTRUCTION_OP_LOAD + | SPV_INSTRUCTION_OP_ACCESS_CHAIN + | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN + ); - let SelectFunctionType(fn_type_id) = function_type; - let fn_id = inc(ib); - let idx_param = inc(ib); - let item_params = (0..length).map(|_| inc(ib)).collect::>(); - let entry_label = inc(ib); let case_labels = (0..length).map(|_| inc(ib)).collect::>(); + let default_label = inc(ib); let merge_label = inc(ib); - let mut spv = vec![]; + let case_temp_ids: Vec = if returns_result { + (0..length).map(|_| inc(ib)).collect() + } else { + vec![] + }; + let default_temp_id = if returns_result { inc(ib) } else { 0 }; - spv.extend_from_slice(&[ - encode_word(5, SPV_INSTRUCTION_OP_FUNCTION), - item_type_id, - fn_id, - SPV_FUNCTION_CONTROL_INLINE, - fn_type_id, - encode_word(3, SPV_INSTRUCTION_OP_FUNCTION_PARAMETER), - uint32_id, - idx_param, - ]); - for &item_param in &item_params { - spv.extend_from_slice(&[ - encode_word(3, SPV_INSTRUCTION_OP_FUNCTION_PARAMETER), - item_type_id, - item_param, - ]); - } + let make_case_instruction = |i: usize| { + let mut patched = original_instruction.to_vec(); + if returns_result { + patched[2] = case_temp_ids[i]; + patched[3] = base_id + i as u32; + } else { + patched[2] = base_id + i as u32; + } + patched + }; + let make_default_instruction = || { + let mut patched = original_instruction.to_vec(); + if returns_result { + patched[2] = default_temp_id; + patched[3] = base_id; + } else { + patched[2] = base_id; + } + patched + }; + + let mut spv = vec![]; spv.extend_from_slice(&[ - encode_word(2, SPV_INSTRUCTION_OP_LABEL), - entry_label, encode_word(3, SPV_INSTRUCTION_OP_SELECTION_MERGE), merge_label, SPV_SELECTION_CONTROL_NONE, encode_word(3 + 2 * length as u16, SPV_INSTRUCTION_OP_SWITCH), - idx_param, - merge_label, + index_id, + default_label, ]); for (i, &case_label) in case_labels.iter().enumerate() { spv.push(i as u32); spv.push(case_label); } - for (case_label, item_param) in case_labels.iter().zip(item_params.iter()) { - spv.extend_from_slice(&[ - encode_word(2, SPV_INSTRUCTION_OP_LABEL), - *case_label, - encode_word(2, SPV_INSTRUCTION_OP_RETURN_VALUE), - *item_param, - ]); + for (i, &case_label) in case_labels.iter().enumerate() { + spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_LABEL), case_label]); + spv.extend_from_slice(&make_case_instruction(i)); + spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_BRANCH), merge_label]); } + spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_LABEL), default_label]); + spv.extend_from_slice(&make_default_instruction()); spv.extend_from_slice(&[ + encode_word(2, SPV_INSTRUCTION_OP_BRANCH), + merge_label, encode_word(2, SPV_INSTRUCTION_OP_LABEL), merge_label, - encode_word(2, SPV_INSTRUCTION_OP_RETURN_VALUE), - item_params[0], - encode_word(1, SPV_INSTRUCTION_OP_FUNCTION_END), ]); - (fn_id, spv) + if returns_result { + let result_type_id = original_instruction[1]; + let original_result_id = original_instruction[2]; + spv.push(encode_word( + 3 + 2 * (length as u16 + 1), + SPV_INSTRUCTION_OP_PHI, + )); + spv.push(result_type_id); + spv.push(original_result_id); + for (&case_label, &temp_id) in case_labels.iter().zip(case_temp_ids.iter()) { + spv.push(temp_id); + spv.push(case_label); + } + spv.push(default_temp_id); + spv.push(default_label); + } + + spv } diff --git a/src/spv.rs b/src/spv.rs index 16a4d6a..2ebc5ce 100644 --- a/src/spv.rs +++ b/src/spv.rs @@ -26,7 +26,9 @@ pub const SPV_INSTRUCTION_OP_FUNCTION: u16 = 54; pub const SPV_INSTRUCTION_OP_VARIABLE: u16 = 59; pub const SPV_INSTRUCTION_OP_LOAD: u16 = 61; pub const SPV_INSTRUCTION_OP_STORE: u16 = 62; +pub const SPV_INSTRUCTION_OP_COPY_MEMORY: u16 = 63; pub const SPV_INSTRUCTION_OP_ACCESS_CHAIN: u16 = 65; +pub const SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN: u16 = 66; pub const SPV_INSTRUCTION_OP_DECORATE: u16 = 71; pub const SPV_INSTRUCTION_OP_MEMBER_DECORATE: u16 = 72; pub const SPV_INSTRUCTION_OP_COMPOSITE_CONSTRUCT: u16 = 80; diff --git a/src/test/splitbindingarray/buffer_binding_array.frag b/src/test/splitbindingarray/buffer_binding_array.frag index 946590c..6599398 100644 --- a/src/test/splitbindingarray/buffer_binding_array.frag +++ b/src/test/splitbindingarray/buffer_binding_array.frag @@ -14,6 +14,8 @@ void main() { for (int i = 0; i < MAX_THINGS; i++) { o_color += u_things[i].thing1; o_color.x += u_things[i].thing2; + + o_color = u_things[i].thing1; } } diff --git a/src/test/splitbindingarray/buffer_binding_array.spv b/src/test/splitbindingarray/buffer_binding_array.spv index a2eac47f3e01113e0cb1a11a651140868db24a68..54c59421a58aa3d641b8f459ac27a8b8a1627bf0 100644 GIT binary patch delta 134 zcmeC+I>W`w%%sfDz`)4B&A`iGx{-G=i=!<^kTwKjIUoj! z^8+zR9Z)NS5fFpq>=>9CI6?f$YgwdKje#tX?gj={uxTbhHb_hjh(Rhrawe1CvPc5} D0wW7W delta 68 zcmX@Z)xpKf%%sfDz`)4B&A`iGw2^l)%j7RC5~>Ca3=Dig+`z!fzyqWWffyvF2E=v@ M%wRdg$%d@b06+-`*8l(j diff --git a/src/test/splitbindingarray/compile.sh b/src/test/splitbindingarray/compile.sh index 0a3a0df..550cae3 100755 --- a/src/test/splitbindingarray/compile.sh +++ b/src/test/splitbindingarray/compile.sh @@ -1,6 +1,6 @@ set -e -glslc buffer_binding_array.frag -o buffer_binding_array.spv -glslc storage_binding_array.frag -o storage_binding_array.spv -glslc texture_binding_array.frag -o texture_binding_array.spv -glslc nested_texture_binding_array.frag -o nested_texture_binding_array.spv +glslc -O0 buffer_binding_array.frag -o buffer_binding_array.spv +glslc -O0 storage_binding_array.frag -o storage_binding_array.spv +glslc -O0 texture_binding_array.frag -o texture_binding_array.spv +glslc -O0 nested_texture_binding_array.frag -o nested_texture_binding_array.spv diff --git a/src/test/splitcombined/compile.sh b/src/test/splitcombined/compile.sh index 41829f6..984c003 100755 --- a/src/test/splitcombined/compile.sh +++ b/src/test/splitcombined/compile.sh @@ -1,7 +1,7 @@ set -e -glslc test.frag -o test.spv -glslc test_arrayed.frag -o test_arrayed.spv -glslc test_nested.frag -o test_nested.spv -glslc test_mixed.frag -o test_mixed.spv +glslc -O0 test.frag -o test.spv +glslc -O0 test_arrayed.frag -o test_arrayed.spv +glslc -O0 test_nested.frag -o test_nested.spv +glslc -O0 test_mixed.frag -o test_mixed.spv diff --git a/src/test/splitdref/compile.sh b/src/test/splitdref/compile.sh index 64aa387..f3f1328 100755 --- a/src/test/splitdref/compile.sh +++ b/src/test/splitdref/compile.sh @@ -1,15 +1,15 @@ set -e -glslc test_image.frag -o test_image.spv -glslc test_nested_image.frag -o test_nested_image.spv -glslc test_nested2_image.frag -o test_nested2_image.spv -glslc test_sampler.frag -o test_sampler.spv -glslc test_nested_sampler.frag -o test_nested_sampler.spv -glslc test_nested2_sampler.frag -o test_nested2_sampler.spv -glslc test_mixed_dref.frag -o test_mixed_dref.spv -glslc test_hidden_dref.frag -o test_hidden_dref.spv -glslc test_hidden2_dref.frag -o test_hidden2_dref.spv -glslc test_hidden3_dref.frag -o test_hidden3_dref.spv -glslc test_cross_dref.frag -o test_cross_dref.spv +glslc -O0 test_image.frag -o test_image.spv +glslc -O0 test_nested_image.frag -o test_nested_image.spv +glslc -O0 test_nested2_image.frag -o test_nested2_image.spv +glslc -O0 test_sampler.frag -o test_sampler.spv +glslc -O0 test_nested_sampler.frag -o test_nested_sampler.spv +glslc -O0 test_nested2_sampler.frag -o test_nested2_sampler.spv +glslc -O0 test_mixed_dref.frag -o test_mixed_dref.spv +glslc -O0 test_hidden_dref.frag -o test_hidden_dref.spv +glslc -O0 test_hidden2_dref.frag -o test_hidden2_dref.spv +glslc -O0 test_hidden3_dref.frag -o test_hidden3_dref.spv +glslc -O0 test_cross_dref.frag -o test_cross_dref.spv spirv-as test_wrong_type_image.spvasm -o test_wrong_type_image.spv diff --git a/src/test/storagecubepatch/compile.sh b/src/test/storagecubepatch/compile.sh index 941b7c7..96de343 100755 --- a/src/test/storagecubepatch/compile.sh +++ b/src/test/storagecubepatch/compile.sh @@ -1,6 +1,6 @@ set -e -glslc storagecube.frag -o storagecube.spv -glslc storagecube_nested.frag -o storagecube_nested.spv -glslc storagecube_immediate.frag -o storagecube_immediate.spv +glslc -O0 storagecube.frag -o storagecube.spv +glslc -O0 storagecube_nested.frag -o storagecube_nested.spv +glslc -O0 storagecube_immediate.frag -o storagecube_immediate.spv From 8a5070162b2e820dc16753f295cc67fd742c7e17 Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 20 May 2026 10:48:47 -0700 Subject: [PATCH 04/16] Implement opaque trace --- src/spv.rs | 2 + src/util.rs | 2 + src/util/opaque_trace.rs | 309 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+) create mode 100644 src/util/opaque_trace.rs diff --git a/src/spv.rs b/src/spv.rs index 2ebc5ce..70459a0 100644 --- a/src/spv.rs +++ b/src/spv.rs @@ -67,8 +67,10 @@ pub const SPV_INSTRUCTION_OP_EXT_INST_IMPORT: u16 = 11; pub const SPV_INSTRUCTION_OP_EXT_INST: u16 = 12; pub const SPV_INSTRUCTION_OP_COMPOSITE_EXTRACT: u16 = 81; pub const SPV_INSTRUCTION_OP_IMAGE_FETCH: u16 = 95; +pub const SPV_INSTRUCTION_OP_IMAGE_TEXEL_POINTER: u16 = 67; pub const SPV_INSTRUCTION_OP_IMAGE_READ: u16 = 98; pub const SPV_INSTRUCTION_OP_IMAGE_WRITE: u16 = 99; +pub const SPV_INSTRUCTION_OP_IMAGE_SPARSE_READ: u16 = 320; pub const SPV_INSTRUCTION_OP_S_NEGATE: u16 = 126; pub const SPV_INSTRUCTION_OP_SELECT: u16 = 169; pub const SPV_INSTRUCTION_OP_S_GREATER_THAN: u16 = 173; diff --git a/src/util.rs b/src/util.rs index f5e569c..10d2dcd 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,12 +4,14 @@ mod correct_decorate; mod decorate; mod ensure; mod function; +mod opaque_trace; mod pointer; pub use correct_decorate::*; pub use decorate::*; pub use ensure::*; pub use function::*; +pub use opaque_trace::*; pub use pointer::*; pub fn hiword(value: u32) -> u16 { diff --git a/src/util/opaque_trace.rs b/src/util/opaque_trace.rs new file mode 100644 index 0000000..7e2747b --- /dev/null +++ b/src/util/opaque_trace.rs @@ -0,0 +1,309 @@ +use super::*; + +// Opaque types cannot be operated on in the same way as non-opaque types. +// We need tools to trace the instruction chain up to the point an opaque type becomes a non-opaque. +// +// We care about OpTypeSampler and OpTypeImage, or more specifically, textures, storage textures, +// and samplers. +// +// My notes on the instruction structure: +// +// ``` +// Textures: +// - OpLoad +// - OpImageFetch +// - OpImageGather +// - OpImageDrefGather +// - OpSampledImage (DAG NODE) +// - OpImageSampleImplicitLod +// - OpImageSampleExplicitLod +// - OpImageSampleDrefImplicitLod +// - OpImageSampleDrefExplicitLod +// - OpImageSampleProjImplicitLod +// - OpImageSampleProjExplicitLod +// - OpImageSampleProjDrefImplicitLod +// - OpImageSampleProjDrefExplicitLod +// - (SparseResidency Capability) +// - OpImageSparseSample* +// OpImageGather +// OpImageDrefGather +// - (ImageQuery Capability) +// - OpImageQuerySizeLod +// - OpImageQuerySize +// - OpImageQueryLevels +// - OpImageQuerySamples +// - OpImageQueryLod +// - OpImageQueryFormat +// - OpImageQueryOrder +// - (SparseResidency Capability) +// - OpImageSparseFetch +// - OpImageSparseGather +// - OpImageSparseDrefGather +// +// Storage Textures: +// - OpLoad +// - OpImageRead +// - OpImageWrite +// - OpImageSparseRead +// - OpImageTexelPointer +// - (ImageQuery Capability) +// - OpImageQuerySizeLod +// - OpImageQuerySize +// - OpImageQueryLevels +// - OpImageQuerySamples +// - OpImageQueryLod +// - OpImageQueryFormat +// - OpImageQueryOrder +// +// Samplers: +// - OpLoad +// - OpSampledImage (DAG NODE) +// ``` +// +// We can build a DAG for the instruction chains, but if we handle sampler's `OpSampledImage` +// separately, we can get away with a tree, or just a `struct` +// + +pub enum OpaqueLoadTrace { + RawImage(RawImageOp), + RawStorage(StorageTextureOp), + Sampled(SampledImageOp), +} + +pub enum RawImageOp { + Fetch(usize), + Gather(usize), + DrefGather(usize), + // TODO: Image Query Capability +} + +pub enum SampledImageOp { + SampleImplicitLod(usize), + SampleExplicitLod(usize), + SampleDrefImplicitLod(usize), + SampleDrefExplicitLod(usize), + SampleProjImplicitLod(usize), + SampleProjExplicitLod(usize), + SampleProjDrefImplicitLod(usize), + SampleProjDrefExplicitLod(usize), + Gather(usize), + DrefGather(usize), + // TODO: Image Query Capability + // TODO: Sparse Residency Capability +} + +pub enum StorageTextureOp { + Read(usize), + Write(usize), + SparseRead(usize), + TexelPointer(usize), + // TODO: Image Query Capability +} + +// Generally, spv[idx + 1] => result type, spv[idx + 2] => result, spv[idx + 3] => image / sampled image +pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec { + let mut op_sampled_image_idxs = vec![]; + let mut raw_image_op_idxs: Vec<(u16, usize)> = vec![]; + let mut sampled_image_op_idxs: Vec<(u16, usize)> = vec![]; + let mut storage_op_idxs: Vec<(u16, usize)> = vec![]; + + let mut spv_idx = 0; + while spv_idx < spv.len() { + let op = spv[spv_idx]; + let word_count = hiword(op) as usize; + let instruction = loword(op); + + match instruction { + SPV_INSTRUCTION_OP_SAMPLED_IMAGE => op_sampled_image_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_IMAGE_FETCH + | SPV_INSTRUCTION_OP_IMAGE_GATHER + | SPV_INSTRUCTION_OP_IMAGE_DREF_GATHER => { + raw_image_op_idxs.push((instruction, spv_idx)) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_IMPLICIT_LOD + | SPV_INSTRUCTION_OP_IMAGE_SAMPLE_EXPLICIT_LOD + | SPV_INSTRUCTION_OP_IMAGE_SAMPLE_DREF_IMPLICIT_LOD + | SPV_INSTRUCTION_OP_IMAGE_SAMPLE_DREF_EXPLICIT_LOD + | SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_IMPLICIT_LOD + | SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_EXPLICIT_LOD + | SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_DREF_IMPLICIT_LOD + | SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_DREF_EXPLICIT_LOD => { + sampled_image_op_idxs.push((instruction, spv_idx)) + } + SPV_INSTRUCTION_OP_IMAGE_READ + | SPV_INSTRUCTION_OP_IMAGE_WRITE + | SPV_INSTRUCTION_OP_IMAGE_SPARSE_READ + | SPV_INSTRUCTION_OP_IMAGE_TEXEL_POINTER => { + storage_op_idxs.push((instruction, spv_idx)) + } + _ => {} + } + + spv_idx += word_count; + } + + let load_result_ids: Vec = load_idxs.iter().map(|&idx| spv[idx + 2]).collect(); + + let mut results = vec![]; + + for &(instruction, idx) in &raw_image_op_idxs { + if load_result_ids.contains(&spv[idx + 3]) { + let op = match instruction { + SPV_INSTRUCTION_OP_IMAGE_FETCH => RawImageOp::Fetch(idx), + SPV_INSTRUCTION_OP_IMAGE_GATHER => RawImageOp::Gather(idx), + SPV_INSTRUCTION_OP_IMAGE_DREF_GATHER => RawImageOp::DrefGather(idx), + _ => unreachable!(), + }; + results.push(OpaqueLoadTrace::RawImage(op)); + } + } + + for &(instruction, idx) in &storage_op_idxs { + let image_id = if instruction == SPV_INSTRUCTION_OP_IMAGE_WRITE { + spv[idx + 1] + } else { + spv[idx + 3] + }; + if load_result_ids.contains(&image_id) { + let op = match instruction { + SPV_INSTRUCTION_OP_IMAGE_READ => StorageTextureOp::Read(idx), + SPV_INSTRUCTION_OP_IMAGE_WRITE => StorageTextureOp::Write(idx), + SPV_INSTRUCTION_OP_IMAGE_SPARSE_READ => StorageTextureOp::SparseRead(idx), + SPV_INSTRUCTION_OP_IMAGE_TEXEL_POINTER => StorageTextureOp::TexelPointer(idx), + _ => unreachable!(), + }; + results.push(OpaqueLoadTrace::RawStorage(op)); + } + } + + let sampled_image_result_ids: Vec = op_sampled_image_idxs + .iter() + .filter(|&&idx| load_result_ids.contains(&spv[idx + 3])) + .map(|&idx| spv[idx + 2]) + .collect(); + + for &(instruction, idx) in &sampled_image_op_idxs { + if sampled_image_result_ids.contains(&spv[idx + 3]) { + let op = match instruction { + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_IMPLICIT_LOD => { + SampledImageOp::SampleImplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_EXPLICIT_LOD => { + SampledImageOp::SampleExplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_DREF_IMPLICIT_LOD => { + SampledImageOp::SampleDrefImplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_DREF_EXPLICIT_LOD => { + SampledImageOp::SampleDrefExplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_IMPLICIT_LOD => { + SampledImageOp::SampleProjImplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_EXPLICIT_LOD => { + SampledImageOp::SampleProjExplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_DREF_IMPLICIT_LOD => { + SampledImageOp::SampleProjDrefImplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_DREF_EXPLICIT_LOD => { + SampledImageOp::SampleProjDrefExplicitLod(idx) + } + _ => unreachable!(), + }; + results.push(OpaqueLoadTrace::Sampled(op)); + } + } + + for &(instruction, idx) in &raw_image_op_idxs { + if sampled_image_result_ids.contains(&spv[idx + 3]) { + let op = match instruction { + SPV_INSTRUCTION_OP_IMAGE_GATHER => SampledImageOp::Gather(idx), + SPV_INSTRUCTION_OP_IMAGE_DREF_GATHER => SampledImageOp::DrefGather(idx), + _ => continue, + }; + results.push(OpaqueLoadTrace::Sampled(op)); + } + } + + results +} + +#[test] +fn raw_image_fetch() { + let spv: &[u32] = &[ + // %20 = OpLoad %10 %30 + encode_word(4, SPV_INSTRUCTION_OP_LOAD), + 10, + 20, + 30, + // %21 = OpImageFetch %11 %20 %40 + encode_word(5, SPV_INSTRUCTION_OP_IMAGE_FETCH), + 11, + 21, + 20, + 40, + ]; + let traces = trace_loaded_opaques(spv, &[0]); + assert_eq!(traces.len(), 1); + assert!(matches!( + traces[0], + OpaqueLoadTrace::RawImage(RawImageOp::Fetch(4)) + )); +} + +#[test] +fn sampled_image_implicit_lod() { + let spv: &[u32] = &[ + // %20 = OpLoad %10 %30 + encode_word(4, SPV_INSTRUCTION_OP_LOAD), + 10, + 20, + 30, + // %21 = OpLoad %11 %31 + encode_word(4, SPV_INSTRUCTION_OP_LOAD), + 11, + 21, + 31, + // %22 = OpSampledImage %12 %20 %21 + encode_word(5, SPV_INSTRUCTION_OP_SAMPLED_IMAGE), + 12, + 22, + 20, + 21, + // %23 = OpImageSampleImplicitLod %13 %22 %40 + encode_word(5, SPV_INSTRUCTION_OP_IMAGE_SAMPLE_IMPLICIT_LOD), + 13, + 23, + 22, + 40, + ]; + let traces = trace_loaded_opaques(spv, &[0]); + assert_eq!(traces.len(), 1); + assert!(matches!( + traces[0], + OpaqueLoadTrace::Sampled(SampledImageOp::SampleImplicitLod(13)) + )); +} + +#[test] +fn storage_image_write() { + let spv: &[u32] = &[ + // %20 = OpLoad %10 %30 + encode_word(4, SPV_INSTRUCTION_OP_LOAD), + 10, + 20, + 30, + // OpImageWrite %20 %40 %50 + encode_word(4, SPV_INSTRUCTION_OP_IMAGE_WRITE), + 20, + 40, + 50, + ]; + let traces = trace_loaded_opaques(spv, &[0]); + assert_eq!(traces.len(), 1); + assert!(matches!( + traces[0], + OpaqueLoadTrace::RawStorage(StorageTextureOp::Write(4)) + )); +} From 1a17cbb46b8c6ed8e2c70387ca33b38a776cb47c Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 20 May 2026 15:09:43 -0700 Subject: [PATCH 05/16] WIP buffer_binding_array pass --- src/splitbindingarray.rs | 178 ++++++++++----- src/splitbindingarray/select_template.rs | 103 +++++---- .../storage_binding_array.frag | 1 + .../storage_binding_array.spv | Bin 1288 -> 1416 bytes src/util/opaque_trace.rs | 206 ++++++++++++++---- 5 files changed, 354 insertions(+), 134 deletions(-) diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index 184b983..5d41666 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -5,6 +5,10 @@ fn inc(ib: &mut u32) -> u32 { *ib - 1 } +// For the purposes of this patch, I consider an OpTypeImage and OpTypeSampler to be opaque. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct OpaqueArrayType; + mod select_template; use select_template::*; @@ -12,6 +16,21 @@ use select_template::*; /// Perform the operation on a `Vec`. /// Use [u8_slice_to_u32_vec] to convert a `&[u8]` into a `Vec` /// Either update the existing `corrections` or create a new one. +/// +/// Assumed SPIR-V properties for this patch: +/// +/// TODO: +/// - No nested +/// - No additional capabilities (SparseResidency or ImageQuery) +/// +/// wgpu Properties: +/// +/// - The only opaque types that can be in an array are `OpTypeImage` and `OpTypeSampler` +/// +/// SPIR-V Properties (These should always be true): +/// - No opaque types in structures +/// - All UBOs and SSBO hold a structure and therefore are accessed with `OpAccessChain*` first. +/// pub fn splitbindingarray( in_spv: &[u32], corrections: &mut Option, @@ -34,6 +53,9 @@ pub fn splitbindingarray( let mut op_type_int_idxs = vec![]; let mut op_type_array_idxs = vec![]; let mut op_type_pointer_idxs = vec![]; + let mut op_type_image_idxs = vec![]; + let mut op_type_sampler_idxs = vec![]; + let mut op_constant_idxs = vec![]; let mut op_variable_idxs = vec![]; let mut op_access_chain_idxs = vec![]; let mut op_in_bounds_access_chain_idxs = vec![]; @@ -58,6 +80,9 @@ pub fn splitbindingarray( SPV_INSTRUCTION_OP_TYPE_INT => op_type_int_idxs.push(spv_idx), SPV_INSTRUCTION_OP_TYPE_ARRAY => op_type_array_idxs.push(spv_idx), SPV_INSTRUCTION_OP_TYPE_POINTER => op_type_pointer_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_TYPE_IMAGE => op_type_image_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_TYPE_SAMPLER => op_type_sampler_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_CONSTANT => op_constant_idxs.push(spv_idx), SPV_INSTRUCTION_OP_VARIABLE => op_variable_idxs.push(spv_idx), SPV_INSTRUCTION_OP_ACCESS_CHAIN => op_access_chain_idxs.push(spv_idx), SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN => { @@ -96,6 +121,12 @@ pub fn splitbindingarray( let tp_storage_class = spv[tp_idx + 2]; let tp_underlying_id = spv[tp_idx + 3]; + let array_type = op_type_image_idxs + .iter() + .chain(op_type_sampler_idxs.iter()) + .any(|&t_idx| spv[t_idx + 1] == tp_underlying_id) + .then_some(OpaqueArrayType); + if tp_storage_class != SPV_STORAGE_CLASS_UNIFORM_CONSTANT && tp_storage_class != SPV_STORAGE_CLASS_UNIFORM { @@ -109,7 +140,7 @@ pub fn splitbindingarray( ta_res_id == tp_underlying_id }) - .map(|&ta_idx| (tp_idx, ta_idx)) + .map(|&ta_idx| (tp_idx, ta_idx, array_type)) }) .collect::>(); @@ -119,15 +150,29 @@ pub fn splitbindingarray( let variable_type_id = spv[v_idx + 1]; array_tp_ta_idxs .iter() - .find(|&(tp_idx, _)| { + .find(|&(tp_idx, _, _)| { let tp_res_id = spv[tp_idx + 1]; tp_res_id == variable_type_id }) - .map(|&(_, ta_idx)| (v_idx, ta_idx)) + .map(|&(_, ta_idx, array_type)| (v_idx, ta_idx, array_type)) }) .collect::>(); - // 3. Unroll array variables + // 3. Build mapping of lengths + let length_map = array_v_ta_idxs + .iter() + .map(|(_, ta_idx, _)| { + let length_id = spv[ta_idx + 3]; + let Some(length) = op_constant_idxs.iter().find_map(|&constant_idx| { + (spv[constant_idx + 2] == length_id).then_some(spv[constant_idx + 3]) + }) else { + panic!("Missing OpConstant") + }; + (ta_idx, length) + }) + .collect::>(); + + // 4. Unroll array variables let types_header_position = last_of_indices!(op_type_int_idxs, op_type_pointer_idxs); let mut types_header_insert = InstructionInsert { previous_spv_idx: types_header_position.unwrap(), @@ -136,7 +181,7 @@ pub fn splitbindingarray( let mut new_variables_map = HashMap::new(); let mut affected_decorations = vec![]; - for &(v_idx, ta_idx) in array_v_ta_idxs.iter() { + for &(v_idx, ta_idx, _) in array_v_ta_idxs.iter() { new_spv[v_idx..v_idx + hiword(spv[v_idx]) as usize] .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); @@ -148,10 +193,12 @@ pub fn splitbindingarray( &op_type_pointer_idxs, &mut instruction_bound, &mut new_instruction, - SPV_STORAGE_CLASS_UNIFORM_CONSTANT, + SPV_STORAGE_CLASS_UNIFORM, underlying_type_id, ); - let length = spv[ta_idx + 3]; + + let length = length_map[&ta_idx]; + let base_id = instruction_bound; instruction_bound += length; @@ -160,7 +207,7 @@ pub fn splitbindingarray( encode_word(4, SPV_INSTRUCTION_OP_VARIABLE), type_pointer_id, base_id + i, - SPV_STORAGE_CLASS_UNIFORM_CONSTANT, + SPV_STORAGE_CLASS_UNIFORM, ]); } // Ordering issues with this, let's keep it after all other type pointers. @@ -182,70 +229,95 @@ pub fn splitbindingarray( } // 5. Replace OpAccessChain with selection function - for (ac_idx, v_idx, ta_idx) in op_access_chain_idxs + for (ac_idx, v_idx, ta_idx, array_type) in op_access_chain_idxs .iter() .chain(op_in_bounds_access_chain_idxs.iter()) .filter_map(|&ac_idx| { let base_id = spv[ac_idx + 3]; array_v_ta_idxs .iter() - .find(|&(v_idx, _)| { + .find(|&(v_idx, _, _)| { let result_id = spv[*v_idx + 2]; result_id == base_id }) - .map(|(v_idx, ta_idx)| (ac_idx, v_idx, ta_idx)) + .map(|(v_idx, ta_idx, array_type)| (ac_idx, v_idx, ta_idx, array_type)) }) { - new_spv[ac_idx..ac_idx + hiword(spv[ac_idx]) as usize] - .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + let ac_word_count = hiword(spv[ac_idx]) as usize; + new_spv[ac_idx..ac_idx + ac_word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); let old_result_id = spv[ac_idx + 2]; let index_0_id = spv[ac_idx + 4]; - let length = spv[ta_idx + 3]; - let base_id = new_variables_map[v_idx]; + let length = length_map[&ta_idx]; - // Find all dependent operations afterwards and replace each instruction with index switch - for &spv_idx in op_load_idxs - .iter() - .chain(op_store_idxs.iter()) - .chain(op_access_chain_idxs.iter()) - .chain(op_in_bounds_access_chain_idxs.iter()) - .chain(op_copy_memory_idxs.iter()) - { - let word_count = hiword(spv[spv_idx]) as usize; - let instruction = loword(spv[spv_idx]); - - let source_offset = match instruction { - SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_COPY_MEMORY => 2, - SPV_INSTRUCTION_OP_LOAD - | SPV_INSTRUCTION_OP_ACCESS_CHAIN - | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN => 3, - _ => unreachable!("Unexpected instruction {} while matching", instruction), - }; + let base_id = new_variables_map[v_idx]; - let source_id = spv[spv_idx + source_offset]; - if source_id == old_result_id && ac_idx != spv_idx { - if instruction == SPV_INSTRUCTION_OP_ACCESS_CHAIN - || instruction == SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN - { - unimplemented!( - "Nested OpAccessChain / OpInBoundsAccessChain on binding array (Unimplemented)" + if let Some(OpaqueArrayType) = *array_type { + // TODO: If opaque, figure out the chain of dependent instructions. + todo!() + } else { + // For concreate types, find all dependent operations afterwards and replace each instruction with index switch + for &spv_idx in op_load_idxs + .iter() + .chain(op_store_idxs.iter()) + .chain(op_access_chain_idxs.iter()) + .chain(op_in_bounds_access_chain_idxs.iter()) + .chain(op_copy_memory_idxs.iter()) + { + let word_count = hiword(spv[spv_idx]) as usize; + let instruction = loword(spv[spv_idx]); + + let is_dependent = match instruction { + SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_COPY_MEMORY => { + // We need to handle cases where buffers are stored from and to. + let source_id = spv[spv_idx + 1]; + let dest_id = spv[spv_idx + 2]; + source_id == old_result_id || dest_id == old_result_id + } + SPV_INSTRUCTION_OP_LOAD + | SPV_INSTRUCTION_OP_ACCESS_CHAIN + | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN => { + let source_id = spv[spv_idx + 3]; + source_id == old_result_id + } + _ => unreachable!("Unexpected instruction {} while matching", instruction), + }; + + if is_dependent && ac_idx != spv_idx { + if instruction == SPV_INSTRUCTION_OP_ACCESS_CHAIN + || instruction == SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN + { + unimplemented!( + "Nested OpAccessChain / OpInBoundsAccessChain on binding array (Unimplemented)" + ); + } + + // We don't want to fully overwrite the access chain since UBOs and SSBOs + // accesses will always be followed by these. + let mut new_instructions = [ + &spv[ac_idx..ac_idx + 4], + &spv[ac_idx + 5..ac_idx + ac_word_count], + &spv[spv_idx..spv_idx + word_count], + ] + .concat(); + new_instructions[0] = + encode_word(ac_word_count as u16 - 1, SPV_INSTRUCTION_OP_ACCESS_CHAIN); + + new_spv[spv_idx..spv_idx + word_count] + .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + let switch = select_template_spv( + &mut instruction_bound, + base_id, + index_0_id, + &new_instructions, + length as usize, ); + instruction_inserts.push(InstructionInsert { + previous_spv_idx: spv_idx, + instruction: switch, + }); } - - new_spv[spv_idx..spv_idx + word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); - let switch = select_template_spv( - &mut instruction_bound, - index_0_id, - base_id, - &spv[spv_idx..spv_idx + word_count], - length as usize, - ); - instruction_inserts.push(InstructionInsert { - previous_spv_idx: spv_idx, - instruction: switch, - }); } } } diff --git a/src/splitbindingarray/select_template.rs b/src/splitbindingarray/select_template.rs index 03ad125..bdaf8f9 100644 --- a/src/splitbindingarray/select_template.rs +++ b/src/splitbindingarray/select_template.rs @@ -9,9 +9,9 @@ use super::*; // pub(super) fn select_template_spv( ib: &mut u32, - index_id: u32, base_id: u32, - original_instruction: &[u32], + index_id: u32, + switch_instructions: &[u32], length: usize, ) -> Vec { // @@ -21,16 +21,20 @@ pub(super) fn select_template_spv( // OpSwitch %index_id %default %merge 0 %case_0 1 %case_1 ... N %case_N // %case_0 = OpLabel // %temp_0 = {INSTRUCTION}(%base_id+0) + // ... // OpBranch %merge // %case_1 = OpLabel // %temp_1 = {INSTRUCTION}(%base_id+1) + // ... // OpBranch %merge // ... // %case_N = OpLabel // %temp_N = {INSTRUCTION}(%base_id+N) + // ... // OpBranch %merge // %default = OpLabel - // {INSTRUCTION}(%base_id+0) + // %temp_0 = {INSTRUCTION}(%base_id+0) + // ... // %merge = OpLabel // // ; Only if there will be a result value. @@ -38,41 +42,59 @@ pub(super) fn select_template_spv( // // - let returns_result = matches!( - loword(original_instruction[0]), - SPV_INSTRUCTION_OP_LOAD - | SPV_INSTRUCTION_OP_ACCESS_CHAIN - | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN - ); + // Parse switch_instructions into per-instruction offsets + let mut instruction_offsets = vec![]; + let mut idx = 0; + while idx < switch_instructions.len() { + instruction_offsets.push(idx); + idx += hiword(switch_instructions[idx]) as usize; + } + + let instruction_returns: Vec = instruction_offsets + .iter() + .map(|&off| { + matches!( + loword(switch_instructions[off]), + SPV_INSTRUCTION_OP_LOAD + | SPV_INSTRUCTION_OP_ACCESS_CHAIN + | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN + ) + }) + .collect(); + + let last_j = instruction_offsets.len() - 1; + let returns_result = instruction_returns[last_j]; let case_labels = (0..length).map(|_| inc(ib)).collect::>(); let default_label = inc(ib); let merge_label = inc(ib); - let case_temp_ids: Vec = if returns_result { - (0..length).map(|_| inc(ib)).collect() - } else { - vec![] - }; - let default_temp_id = if returns_result { inc(ib) } else { 0 }; + // Per-case and default temp IDs, one slot per instruction (0 if no result) + let case_temps: Vec> = (0..length) + .map(|_| { + instruction_returns + .iter() + .map(|&ret| if ret { inc(ib) } else { 0 }) + .collect() + }) + .collect(); + let default_temps: Vec = instruction_returns + .iter() + .map(|&ret| if ret { inc(ib) } else { 0 }) + .collect(); - let make_case_instruction = |i: usize| { - let mut patched = original_instruction.to_vec(); - if returns_result { - patched[2] = case_temp_ids[i]; - patched[3] = base_id + i as u32; - } else { - patched[2] = base_id + i as u32; - } - patched - }; - let make_default_instruction = || { - let mut patched = original_instruction.to_vec(); - if returns_result { - patched[2] = default_temp_id; - patched[3] = base_id; - } else { - patched[2] = base_id; + // Build the patched instruction sequence, chaining each result into the next source + let make_instructions = |temps: &[u32], base: u32| -> Vec { + let mut patched = switch_instructions.to_vec(); + let mut current_source = base; + for (j, &off) in instruction_offsets.iter().enumerate() { + if instruction_returns[j] { + patched[off + 2] = temps[j]; + patched[off + 3] = current_source; + current_source = temps[j]; + } else { + patched[off + 2] = current_source; + } } patched }; @@ -94,12 +116,12 @@ pub(super) fn select_template_spv( for (i, &case_label) in case_labels.iter().enumerate() { spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_LABEL), case_label]); - spv.extend_from_slice(&make_case_instruction(i)); + spv.extend_from_slice(&make_instructions(&case_temps[i], base_id + i as u32)); spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_BRANCH), merge_label]); } spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_LABEL), default_label]); - spv.extend_from_slice(&make_default_instruction()); + spv.extend_from_slice(&make_instructions(&default_temps, base_id)); spv.extend_from_slice(&[ encode_word(2, SPV_INSTRUCTION_OP_BRANCH), merge_label, @@ -108,19 +130,20 @@ pub(super) fn select_template_spv( ]); if returns_result { - let result_type_id = original_instruction[1]; - let original_result_id = original_instruction[2]; + let last_off = instruction_offsets[last_j]; + let result_type_id = switch_instructions[last_off + 1]; + let result_id = switch_instructions[last_off + 2]; spv.push(encode_word( 3 + 2 * (length as u16 + 1), SPV_INSTRUCTION_OP_PHI, )); spv.push(result_type_id); - spv.push(original_result_id); - for (&case_label, &temp_id) in case_labels.iter().zip(case_temp_ids.iter()) { - spv.push(temp_id); + spv.push(result_id); + for (i, &case_label) in case_labels.iter().enumerate() { + spv.push(case_temps[i][last_j]); spv.push(case_label); } - spv.push(default_temp_id); + spv.push(default_temps[last_j]); spv.push(default_label); } diff --git a/src/test/splitbindingarray/storage_binding_array.frag b/src/test/splitbindingarray/storage_binding_array.frag index a87f6a6..16ec4cd 100644 --- a/src/test/splitbindingarray/storage_binding_array.frag +++ b/src/test/splitbindingarray/storage_binding_array.frag @@ -14,6 +14,7 @@ void main() { for (int i = 0; i < MAX_THINGS; i++) { o_color += u_things[i].thing1; o_color.x += u_things[i].thing2; + u_things[i].thing2 *= 2.0; } } diff --git a/src/test/splitbindingarray/storage_binding_array.spv b/src/test/splitbindingarray/storage_binding_array.spv index 8b7e303a8cecd51b167c09a6fd274132cfaee6ee..b515c3a159aee6a7f46659b103e78a94eea6ab09 100644 GIT binary patch delta 218 zcmeC+>fq*OW>RKnU|?k6X5eM8*vR{vNkE%{g@KKMfx!@nfxuxi2lEQXdRw3<50GmB z#C$;P2vnmEq>X@B4v0bGYCsH92hwc}#2~p=239cN1S$qH6{gn|$VS#{$H2@05(BCI s$-u-Q45WWR=*jz8I91Jne30D@Kr=yZF$ZFh7|33bUXYymRKnU|?k6X5eKo+Q|ExX|n|L3dYF>tP-jQ3=9l>K-|E<%D@Ap4S^UW RrUt}z49s9T!^yd<(g1dC2-*Mu diff --git a/src/util/opaque_trace.rs b/src/util/opaque_trace.rs index 7e2747b..13917ca 100644 --- a/src/util/opaque_trace.rs +++ b/src/util/opaque_trace.rs @@ -77,7 +77,21 @@ pub enum RawImageOp { // TODO: Image Query Capability } -pub enum SampledImageOp { +impl RawImageOp { + pub fn result_type_and_id(&self, spv: &[u32]) -> (u32, u32) { + let idx = match self { + RawImageOp::Fetch(i) | RawImageOp::Gather(i) | RawImageOp::DrefGather(i) => *i, + }; + (spv[idx + 1], spv[idx + 2]) + } +} + +pub struct SampledImageOp { + pub idx: usize, + pub next: SampledImageVariant, +} + +pub enum SampledImageVariant { SampleImplicitLod(usize), SampleExplicitLod(usize), SampleDrefImplicitLod(usize), @@ -92,6 +106,24 @@ pub enum SampledImageOp { // TODO: Sparse Residency Capability } +impl SampledImageVariant { + pub fn result(&self, spv: &[u32]) -> (u32, u32) { + let idx = match self { + SampledImageVariant::SampleImplicitLod(i) + | SampledImageVariant::SampleExplicitLod(i) + | SampledImageVariant::SampleDrefImplicitLod(i) + | SampledImageVariant::SampleDrefExplicitLod(i) + | SampledImageVariant::SampleProjImplicitLod(i) + | SampledImageVariant::SampleProjExplicitLod(i) + | SampledImageVariant::SampleProjDrefImplicitLod(i) + | SampledImageVariant::SampleProjDrefExplicitLod(i) + | SampledImageVariant::Gather(i) + | SampledImageVariant::DrefGather(i) => *i, + }; + (spv[idx + 1], spv[idx + 2]) + } +} + pub enum StorageTextureOp { Read(usize), Write(usize), @@ -100,6 +132,19 @@ pub enum StorageTextureOp { // TODO: Image Query Capability } +impl StorageTextureOp { + // OpImageWrite has no result; all other storage ops do. + pub fn result(&self, spv: &[u32]) -> Option<(u32, u32)> { + let idx = match self { + StorageTextureOp::Read(i) + | StorageTextureOp::SparseRead(i) + | StorageTextureOp::TexelPointer(i) => *i, + StorageTextureOp::Write(_) => return None, + }; + Some((spv[idx + 1], spv[idx + 2])) + } +} + // Generally, spv[idx + 1] => result type, spv[idx + 2] => result, spv[idx + 3] => image / sampled image pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec { let mut op_sampled_image_idxs = vec![]; @@ -176,57 +221,133 @@ pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec = op_sampled_image_idxs + // (result_id, sampled_image_idx) for OpSampledImage nodes rooted at our loads + let sampled_image_entries: Vec<(u32, usize)> = op_sampled_image_idxs .iter() .filter(|&&idx| load_result_ids.contains(&spv[idx + 3])) - .map(|&idx| spv[idx + 2]) + .map(|&idx| (spv[idx + 2], idx)) .collect(); for &(instruction, idx) in &sampled_image_op_idxs { - if sampled_image_result_ids.contains(&spv[idx + 3]) { - let op = match instruction { - SPV_INSTRUCTION_OP_IMAGE_SAMPLE_IMPLICIT_LOD => { - SampledImageOp::SampleImplicitLod(idx) - } - SPV_INSTRUCTION_OP_IMAGE_SAMPLE_EXPLICIT_LOD => { - SampledImageOp::SampleExplicitLod(idx) - } - SPV_INSTRUCTION_OP_IMAGE_SAMPLE_DREF_IMPLICIT_LOD => { - SampledImageOp::SampleDrefImplicitLod(idx) - } - SPV_INSTRUCTION_OP_IMAGE_SAMPLE_DREF_EXPLICIT_LOD => { - SampledImageOp::SampleDrefExplicitLod(idx) - } - SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_IMPLICIT_LOD => { - SampledImageOp::SampleProjImplicitLod(idx) - } - SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_EXPLICIT_LOD => { - SampledImageOp::SampleProjExplicitLod(idx) - } - SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_DREF_IMPLICIT_LOD => { - SampledImageOp::SampleProjDrefImplicitLod(idx) - } - SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_DREF_EXPLICIT_LOD => { - SampledImageOp::SampleProjDrefExplicitLod(idx) - } - _ => unreachable!(), - }; - results.push(OpaqueLoadTrace::Sampled(op)); - } + let Some(&(_, si_idx)) = sampled_image_entries + .iter() + .find(|(result_id, _)| *result_id == spv[idx + 3]) + else { + continue; + }; + let variant = match instruction { + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_IMPLICIT_LOD => { + SampledImageVariant::SampleImplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_EXPLICIT_LOD => { + SampledImageVariant::SampleExplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_DREF_IMPLICIT_LOD => { + SampledImageVariant::SampleDrefImplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_DREF_EXPLICIT_LOD => { + SampledImageVariant::SampleDrefExplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_IMPLICIT_LOD => { + SampledImageVariant::SampleProjImplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_EXPLICIT_LOD => { + SampledImageVariant::SampleProjExplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_DREF_IMPLICIT_LOD => { + SampledImageVariant::SampleProjDrefImplicitLod(idx) + } + SPV_INSTRUCTION_OP_IMAGE_SAMPLE_PROJ_DREF_EXPLICIT_LOD => { + SampledImageVariant::SampleProjDrefExplicitLod(idx) + } + _ => unreachable!(), + }; + results.push(OpaqueLoadTrace::Sampled(SampledImageOp { + idx: si_idx, + next: variant, + })); } for &(instruction, idx) in &raw_image_op_idxs { - if sampled_image_result_ids.contains(&spv[idx + 3]) { - let op = match instruction { - SPV_INSTRUCTION_OP_IMAGE_GATHER => SampledImageOp::Gather(idx), - SPV_INSTRUCTION_OP_IMAGE_DREF_GATHER => SampledImageOp::DrefGather(idx), - _ => continue, + let Some(&(_, si_idx)) = sampled_image_entries + .iter() + .find(|(result_id, _)| *result_id == spv[idx + 3]) + else { + continue; + }; + let variant = match instruction { + SPV_INSTRUCTION_OP_IMAGE_GATHER => SampledImageVariant::Gather(idx), + SPV_INSTRUCTION_OP_IMAGE_DREF_GATHER => SampledImageVariant::DrefGather(idx), + _ => continue, + }; + results.push(OpaqueLoadTrace::Sampled(SampledImageOp { + idx: si_idx, + next: variant, + })); + } + + results +} + +pub fn reconstruct_opaque_trace_and_overwrite( + spv: &[u32], + new_spv: &mut [u32], + load_idx: usize, + trace: &OpaqueLoadTrace, +) -> Vec { + fn take_instruction(spv: &[u32], idx: usize) -> &[u32] { + let word_count = hiword(spv[idx]) as usize; + &spv[idx..idx + word_count] + } + + fn write_nop_instruction(new_spv: &mut [u32], idx: usize) { + let word_count = hiword(new_spv[idx]) as usize; + new_spv[idx..idx + word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + } + + let mut out = take_instruction(spv, load_idx).to_vec(); + write_nop_instruction(new_spv, load_idx); + + match trace { + OpaqueLoadTrace::RawImage(op) => { + let op_idx = match op { + RawImageOp::Fetch(i) | RawImageOp::Gather(i) | RawImageOp::DrefGather(i) => *i, }; - results.push(OpaqueLoadTrace::Sampled(op)); + out.extend_from_slice(take_instruction(spv, op_idx)); + write_nop_instruction(new_spv, op_idx); + } + OpaqueLoadTrace::RawStorage(op) => { + let op_idx = match op { + StorageTextureOp::Read(i) + | StorageTextureOp::Write(i) + | StorageTextureOp::SparseRead(i) + | StorageTextureOp::TexelPointer(i) => *i, + }; + out.extend_from_slice(take_instruction(spv, op_idx)); + write_nop_instruction(new_spv, op_idx); + } + OpaqueLoadTrace::Sampled(SampledImageOp { idx: si_idx, next }) => { + out.extend_from_slice(take_instruction(spv, *si_idx)); + write_nop_instruction(new_spv, *si_idx); + + let op_idx = match next { + SampledImageVariant::SampleImplicitLod(i) + | SampledImageVariant::SampleExplicitLod(i) + | SampledImageVariant::SampleDrefImplicitLod(i) + | SampledImageVariant::SampleDrefExplicitLod(i) + | SampledImageVariant::SampleProjImplicitLod(i) + | SampledImageVariant::SampleProjExplicitLod(i) + | SampledImageVariant::SampleProjDrefImplicitLod(i) + | SampledImageVariant::SampleProjDrefExplicitLod(i) + | SampledImageVariant::Gather(i) + | SampledImageVariant::DrefGather(i) => *i, + }; + out.extend_from_slice(take_instruction(spv, op_idx)); + write_nop_instruction(new_spv, op_idx); } } - results + out } #[test] @@ -282,7 +403,10 @@ fn sampled_image_implicit_lod() { assert_eq!(traces.len(), 1); assert!(matches!( traces[0], - OpaqueLoadTrace::Sampled(SampledImageOp::SampleImplicitLod(13)) + OpaqueLoadTrace::Sampled(SampledImageOp { + idx: 8, + next: SampledImageVariant::SampleImplicitLod(13), + }) )); } From c11d3b657aa2093705bfa07e43173054cddddf0b Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 20 May 2026 19:51:15 -0700 Subject: [PATCH 06/16] WIP OpStore in both directions --- src/splitbindingarray.rs | 8 ++++ src/splitbindingarray/select_template.rs | 56 ++++++++++++++---------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index 5d41666..43fe569 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -268,11 +268,18 @@ pub fn splitbindingarray( let word_count = hiword(spv[spv_idx]) as usize; let instruction = loword(spv[spv_idx]); + let mut flip_store_into = false; let is_dependent = match instruction { SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_COPY_MEMORY => { // We need to handle cases where buffers are stored from and to. let source_id = spv[spv_idx + 1]; let dest_id = spv[spv_idx + 2]; + + // OpStore: %result = %a + if dest_id == old_result_id { + flip_store_into = true; + } + source_id == old_result_id || dest_id == old_result_id } SPV_INSTRUCTION_OP_LOAD @@ -312,6 +319,7 @@ pub fn splitbindingarray( index_0_id, &new_instructions, length as usize, + flip_store_into, ); instruction_inserts.push(InstructionInsert { previous_spv_idx: spv_idx, diff --git a/src/splitbindingarray/select_template.rs b/src/splitbindingarray/select_template.rs index bdaf8f9..9900fea 100644 --- a/src/splitbindingarray/select_template.rs +++ b/src/splitbindingarray/select_template.rs @@ -7,12 +7,15 @@ use super::*; // The instruction's source, `[idx+2]` is replaced with `%base_id+N` // and duplicated for each case of the index, see template below. // +// `flip_store_into` specifically changes `%a = %result` to `%result = %a` +// pub(super) fn select_template_spv( ib: &mut u32, base_id: u32, index_id: u32, switch_instructions: &[u32], length: usize, + flip_store_into: bool, ) -> Vec { // // TODO: You can probably decrease the instruction count with OpPhi or OpSelect. @@ -40,9 +43,7 @@ pub(super) fn select_template_spv( // ; Only if there will be a result value. // %target_id = OpPhi %underlying_type_id %temp_0 %case_0 %temp_1 %case_1 ... %temp_N %case_N // - // - // Parse switch_instructions into per-instruction offsets let mut instruction_offsets = vec![]; let mut idx = 0; while idx < switch_instructions.len() { @@ -50,50 +51,58 @@ pub(super) fn select_template_spv( idx += hiword(switch_instructions[idx]) as usize; } - let instruction_returns: Vec = instruction_offsets - .iter() - .map(|&off| { - matches!( - loword(switch_instructions[off]), - SPV_INSTRUCTION_OP_LOAD - | SPV_INSTRUCTION_OP_ACCESS_CHAIN - | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN - ) - }) - .collect(); - let last_j = instruction_offsets.len() - 1; - let returns_result = instruction_returns[last_j]; + let last_off = instruction_offsets[last_j]; + let returns_result = matches!( + loword(switch_instructions[last_off]), + SPV_INSTRUCTION_OP_LOAD + | SPV_INSTRUCTION_OP_ACCESS_CHAIN + | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN + ); let case_labels = (0..length).map(|_| inc(ib)).collect::>(); let default_label = inc(ib); let merge_label = inc(ib); - // Per-case and default temp IDs, one slot per instruction (0 if no result) let case_temps: Vec> = (0..length) .map(|_| { - instruction_returns + instruction_offsets .iter() - .map(|&ret| if ret { inc(ib) } else { 0 }) + .enumerate() + .map(|(j, _)| { + if j < last_j || returns_result { + inc(ib) + } else { + 0 + } + }) .collect() }) .collect(); - let default_temps: Vec = instruction_returns + let default_temps: Vec = instruction_offsets .iter() - .map(|&ret| if ret { inc(ib) } else { 0 }) + .enumerate() + .map(|(j, _)| { + if j < last_j || returns_result { + inc(ib) + } else { + 0 + } + }) .collect(); - // Build the patched instruction sequence, chaining each result into the next source let make_instructions = |temps: &[u32], base: u32| -> Vec { let mut patched = switch_instructions.to_vec(); let mut current_source = base; for (j, &off) in instruction_offsets.iter().enumerate() { - if instruction_returns[j] { + if j < last_j || returns_result { patched[off + 2] = temps[j]; patched[off + 3] = current_source; current_source = temps[j]; - } else { + } else if flip_store_into { patched[off + 2] = current_source; + } else { + patched[off + 1] = current_source; } } patched @@ -130,7 +139,6 @@ pub(super) fn select_template_spv( ]); if returns_result { - let last_off = instruction_offsets[last_j]; let result_type_id = switch_instructions[last_off + 1]; let result_id = switch_instructions[last_off + 2]; spv.push(encode_word( From e50164239c3fafb305dbb6ae1d04299e2d1ae7cd Mon Sep 17 00:00:00 2001 From: davnotdev Date: Thu, 21 May 2026 10:09:29 -0700 Subject: [PATCH 07/16] WIP opaque images --- src/splitbindingarray.rs | 55 ++++++-- src/splitbindingarray/select_template.rs | 15 ++- src/test.rs | 24 +++- src/util/opaque_trace.rs | 156 +++++++++++++++-------- 4 files changed, 178 insertions(+), 72 deletions(-) diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index 43fe569..4d9a795 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -121,12 +121,6 @@ pub fn splitbindingarray( let tp_storage_class = spv[tp_idx + 2]; let tp_underlying_id = spv[tp_idx + 3]; - let array_type = op_type_image_idxs - .iter() - .chain(op_type_sampler_idxs.iter()) - .any(|&t_idx| spv[t_idx + 1] == tp_underlying_id) - .then_some(OpaqueArrayType); - if tp_storage_class != SPV_STORAGE_CLASS_UNIFORM_CONSTANT && tp_storage_class != SPV_STORAGE_CLASS_UNIFORM { @@ -140,7 +134,15 @@ pub fn splitbindingarray( ta_res_id == tp_underlying_id }) - .map(|&ta_idx| (tp_idx, ta_idx, array_type)) + .map(|&ta_idx| { + let array_type = op_type_image_idxs + .iter() + .chain(op_type_sampler_idxs.iter()) + .any(|&t_idx| spv[t_idx + 1] == spv[ta_idx + 2]) + .then_some(OpaqueArrayType); + + (tp_idx, ta_idx, array_type) + }) }) .collect::>(); @@ -181,7 +183,7 @@ pub fn splitbindingarray( let mut new_variables_map = HashMap::new(); let mut affected_decorations = vec![]; - for &(v_idx, ta_idx, _) in array_v_ta_idxs.iter() { + for &(v_idx, ta_idx, array_type) in array_v_ta_idxs.iter() { new_spv[v_idx..v_idx + hiword(spv[v_idx]) as usize] .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); @@ -193,7 +195,10 @@ pub fn splitbindingarray( &op_type_pointer_idxs, &mut instruction_bound, &mut new_instruction, - SPV_STORAGE_CLASS_UNIFORM, + match array_type { + Some(OpaqueArrayType) => SPV_STORAGE_CLASS_UNIFORM_CONSTANT, + _ => SPV_STORAGE_CLASS_UNIFORM, + }, underlying_type_id, ); @@ -207,7 +212,10 @@ pub fn splitbindingarray( encode_word(4, SPV_INSTRUCTION_OP_VARIABLE), type_pointer_id, base_id + i, - SPV_STORAGE_CLASS_UNIFORM, + match array_type { + Some(OpaqueArrayType) => SPV_STORAGE_CLASS_UNIFORM_CONSTANT, + _ => SPV_STORAGE_CLASS_UNIFORM, + }, ]); } // Ordering issues with this, let's keep it after all other type pointers. @@ -254,8 +262,31 @@ pub fn splitbindingarray( let base_id = new_variables_map[v_idx]; if let Some(OpaqueArrayType) = *array_type { - // TODO: If opaque, figure out the chain of dependent instructions. - todo!() + let load_idxs = op_load_idxs + .iter() + .filter(|&idx| { + let pointer = spv[idx + 3]; + pointer == old_result_id + }) + .copied() + .collect::>(); + let dependent_traces = trace_loaded_opaques(&spv, &load_idxs); + for trace in dependent_traces { + let switch_instructions = + reconstruct_opaque_trace_and_overwrite(&spv, &mut new_spv, &trace); + let switch = select_template_spv( + &mut instruction_bound, + base_id, + index_0_id, + &switch_instructions, + length as usize, + false, + ); + instruction_inserts.push(InstructionInsert { + previous_spv_idx: trace.last_result_id(), + instruction: switch, + }); + } } else { // For concreate types, find all dependent operations afterwards and replace each instruction with index switch for &spv_idx in op_load_idxs diff --git a/src/splitbindingarray/select_template.rs b/src/splitbindingarray/select_template.rs index 9900fea..8266ed0 100644 --- a/src/splitbindingarray/select_template.rs +++ b/src/splitbindingarray/select_template.rs @@ -1,12 +1,14 @@ use super::*; -// Take the following instructions: -// OpLoad, OpStore, OpAccessChain, OpInBoundsAccessChain, OpCopyMemory -// (i) (i) (i) +// Take the any chain of instructions with the following form: +// OpSomething %result_type_id %result_id %input ... // // The instruction's source, `[idx+2]` is replaced with `%base_id+N` // and duplicated for each case of the index, see template below. // +// Write instructions are specially checked for because they follow a different convention: +// OpStore, OpCopyMemory, OpImageWrite +// // `flip_store_into` specifically changes `%a = %result` to `%result = %a` // pub(super) fn select_template_spv( @@ -53,11 +55,10 @@ pub(super) fn select_template_spv( let last_j = instruction_offsets.len() - 1; let last_off = instruction_offsets[last_j]; - let returns_result = matches!( + + let returns_result = !matches!( loword(switch_instructions[last_off]), - SPV_INSTRUCTION_OP_LOAD - | SPV_INSTRUCTION_OP_ACCESS_CHAIN - | SPV_INSTRUCTION_OP_IN_BOUNDS_ACCESS_CHAIN + SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_IMAGE_WRITE | SPV_INSTRUCTION_OP_COPY_MEMORY ); let case_labels = (0..length).map(|_| inc(ib)).collect::>(); diff --git a/src/test.rs b/src/test.rs index a38bc15..86bef1c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,6 +1,6 @@ use super::{ combimgsampsplitter, drefsplitter, immediatespatch, isnanisinfpatch, mirrorpatch, - pruneunuseddref, storagecubepatch, u8_slice_to_u32_vec, u32_slice_to_u8_vec, + pruneunuseddref, splitbindingarray, storagecubepatch, u8_slice_to_u32_vec, u32_slice_to_u8_vec, }; use naga::{back, front, valid}; @@ -222,6 +222,7 @@ test_with_spv_and_fn_no_correction![ ]; // --- + test_with_spv_and_fn_no_correction![ immediatespatch_immediatespatch_immediates, DO_ALL, @@ -254,3 +255,24 @@ test_with_spv_and_fn_no_correction![ "./test/immediatespatch/row_major.spv", immediatespatch ]; + +// --- + +test_with_spv_and_fn![ + splitbinding_splitbinding_buffer_binding_array, + DO_ALL, + "./test/splitbindingarray/buffer_binding_array.spv", + splitbindingarray +]; +test_with_spv_and_fn![ + splitbinding_splitbinding_storage_binding_array, + DO_ALL, + "./test/splitbindingarray/storage_binding_array.spv", + splitbindingarray +]; +test_with_spv_and_fn![ + splitbinding_splitbinding_texture_binding_array, + DO_ALL, + "./test/splitbindingarray/texture_binding_array.spv", + splitbindingarray +]; diff --git a/src/util/opaque_trace.rs b/src/util/opaque_trace.rs index 13917ca..a59d4b0 100644 --- a/src/util/opaque_trace.rs +++ b/src/util/opaque_trace.rs @@ -64,12 +64,30 @@ use super::*; // separately, we can get away with a tree, or just a `struct` // -pub enum OpaqueLoadTrace { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OpaqueLoadTrace { + pub load_idx: usize, + pub next: OpaqueImageOp, +} + +impl OpaqueLoadTrace { + pub fn last_result_id(&self) -> usize { + match self.next { + OpaqueImageOp::RawImage(raw_image_op) => raw_image_op.result_idx(), + OpaqueImageOp::RawStorage(storage_texture_op) => storage_texture_op.result_idx(), + OpaqueImageOp::Sampled(sampled_image_op) => sampled_image_op.next.result_idx(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpaqueImageOp { RawImage(RawImageOp), RawStorage(StorageTextureOp), Sampled(SampledImageOp), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RawImageOp { Fetch(usize), Gather(usize), @@ -78,19 +96,20 @@ pub enum RawImageOp { } impl RawImageOp { - pub fn result_type_and_id(&self, spv: &[u32]) -> (u32, u32) { - let idx = match self { + pub fn result_idx(&self) -> usize { + match self { RawImageOp::Fetch(i) | RawImageOp::Gather(i) | RawImageOp::DrefGather(i) => *i, - }; - (spv[idx + 1], spv[idx + 2]) + } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SampledImageOp { pub idx: usize, pub next: SampledImageVariant, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SampledImageVariant { SampleImplicitLod(usize), SampleExplicitLod(usize), @@ -107,8 +126,8 @@ pub enum SampledImageVariant { } impl SampledImageVariant { - pub fn result(&self, spv: &[u32]) -> (u32, u32) { - let idx = match self { + pub fn result_idx(&self) -> usize { + match self { SampledImageVariant::SampleImplicitLod(i) | SampledImageVariant::SampleExplicitLod(i) | SampledImageVariant::SampleDrefImplicitLod(i) @@ -119,11 +138,11 @@ impl SampledImageVariant { | SampledImageVariant::SampleProjDrefExplicitLod(i) | SampledImageVariant::Gather(i) | SampledImageVariant::DrefGather(i) => *i, - }; - (spv[idx + 1], spv[idx + 2]) + } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StorageTextureOp { Read(usize), Write(usize), @@ -134,19 +153,19 @@ pub enum StorageTextureOp { impl StorageTextureOp { // OpImageWrite has no result; all other storage ops do. - pub fn result(&self, spv: &[u32]) -> Option<(u32, u32)> { - let idx = match self { + pub fn result_idx(&self) -> usize { + match self { StorageTextureOp::Read(i) | StorageTextureOp::SparseRead(i) - | StorageTextureOp::TexelPointer(i) => *i, - StorageTextureOp::Write(_) => return None, - }; - Some((spv[idx + 1], spv[idx + 2])) + | StorageTextureOp::TexelPointer(i) + | StorageTextureOp::Write(i) => *i, + } } } // Generally, spv[idx + 1] => result type, spv[idx + 2] => result, spv[idx + 3] => image / sampled image pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec { + // TODO: Memoize, we can do better than this. let mut op_sampled_image_idxs = vec![]; let mut raw_image_op_idxs: Vec<(u16, usize)> = vec![]; let mut sampled_image_op_idxs: Vec<(u16, usize)> = vec![]; @@ -187,19 +206,26 @@ pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec = load_idxs.iter().map(|&idx| spv[idx + 2]).collect(); + let load_result_ids = load_idxs + .iter() + .map(|&idx| (spv[idx + 2], idx)) + .collect::>(); let mut results = vec![]; for &(instruction, idx) in &raw_image_op_idxs { - if load_result_ids.contains(&spv[idx + 3]) { + let loaded_image_id = spv[idx + 3]; + if let Some(&load_idx) = load_result_ids.get(&loaded_image_id) { let op = match instruction { SPV_INSTRUCTION_OP_IMAGE_FETCH => RawImageOp::Fetch(idx), SPV_INSTRUCTION_OP_IMAGE_GATHER => RawImageOp::Gather(idx), SPV_INSTRUCTION_OP_IMAGE_DREF_GATHER => RawImageOp::DrefGather(idx), _ => unreachable!(), }; - results.push(OpaqueLoadTrace::RawImage(op)); + results.push(OpaqueLoadTrace { + load_idx, + next: OpaqueImageOp::RawImage(op), + }); } } @@ -209,7 +235,7 @@ pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec StorageTextureOp::Read(idx), SPV_INSTRUCTION_OP_IMAGE_WRITE => StorageTextureOp::Write(idx), @@ -217,21 +243,31 @@ pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec StorageTextureOp::TexelPointer(idx), _ => unreachable!(), }; - results.push(OpaqueLoadTrace::RawStorage(op)); + results.push(OpaqueLoadTrace { + load_idx, + next: OpaqueImageOp::RawStorage(op), + }); } } // (result_id, sampled_image_idx) for OpSampledImage nodes rooted at our loads - let sampled_image_entries: Vec<(u32, usize)> = op_sampled_image_idxs + let sampled_image_entries = op_sampled_image_idxs .iter() - .filter(|&&idx| load_result_ids.contains(&spv[idx + 3])) - .map(|&idx| (spv[idx + 2], idx)) - .collect(); + .filter_map(|&idx| { + load_result_ids + .get(&spv[idx + 3]) + .copied() + .map(|load_idx| (idx, load_idx)) + }) + .map(|(idx, load_idx)| (spv[idx + 2], idx, load_idx)) + .collect::>(); - for &(instruction, idx) in &sampled_image_op_idxs { - let Some(&(_, si_idx)) = sampled_image_entries - .iter() - .find(|(result_id, _)| *result_id == spv[idx + 3]) + for &(instruction, idx) in sampled_image_op_idxs.iter() { + let Some(&(_, si_idx, load_idx)) = + sampled_image_entries.iter().find(|(result_id, _, _)| { + let loaded_image_id = spv[idx + 3]; + *result_id == loaded_image_id + }) else { continue; }; @@ -262,16 +298,21 @@ pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec unreachable!(), }; - results.push(OpaqueLoadTrace::Sampled(SampledImageOp { - idx: si_idx, - next: variant, - })); + results.push(OpaqueLoadTrace { + load_idx, + next: OpaqueImageOp::Sampled(SampledImageOp { + idx: si_idx, + next: variant, + }), + }); } for &(instruction, idx) in &raw_image_op_idxs { - let Some(&(_, si_idx)) = sampled_image_entries - .iter() - .find(|(result_id, _)| *result_id == spv[idx + 3]) + let Some(&(_, si_idx, load_idx)) = + sampled_image_entries.iter().find(|(result_id, _, _)| { + let loaded_image_id = spv[idx + 3]; + *result_id == loaded_image_id + }) else { continue; }; @@ -280,10 +321,13 @@ pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec SampledImageVariant::DrefGather(idx), _ => continue, }; - results.push(OpaqueLoadTrace::Sampled(SampledImageOp { - idx: si_idx, - next: variant, - })); + results.push(OpaqueLoadTrace { + load_idx, + next: OpaqueImageOp::Sampled(SampledImageOp { + idx: si_idx, + next: variant, + }), + }); } results @@ -292,7 +336,6 @@ pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec Vec { fn take_instruction(spv: &[u32], idx: usize) -> &[u32] { @@ -305,18 +348,18 @@ pub fn reconstruct_opaque_trace_and_overwrite( new_spv[idx..idx + word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); } - let mut out = take_instruction(spv, load_idx).to_vec(); - write_nop_instruction(new_spv, load_idx); + let mut out = take_instruction(spv, trace.load_idx).to_vec(); + write_nop_instruction(new_spv, trace.load_idx); - match trace { - OpaqueLoadTrace::RawImage(op) => { + match &trace.next { + OpaqueImageOp::RawImage(op) => { let op_idx = match op { RawImageOp::Fetch(i) | RawImageOp::Gather(i) | RawImageOp::DrefGather(i) => *i, }; out.extend_from_slice(take_instruction(spv, op_idx)); write_nop_instruction(new_spv, op_idx); } - OpaqueLoadTrace::RawStorage(op) => { + OpaqueImageOp::RawStorage(op) => { let op_idx = match op { StorageTextureOp::Read(i) | StorageTextureOp::Write(i) @@ -326,7 +369,7 @@ pub fn reconstruct_opaque_trace_and_overwrite( out.extend_from_slice(take_instruction(spv, op_idx)); write_nop_instruction(new_spv, op_idx); } - OpaqueLoadTrace::Sampled(SampledImageOp { idx: si_idx, next }) => { + OpaqueImageOp::Sampled(SampledImageOp { idx: si_idx, next }) => { out.extend_from_slice(take_instruction(spv, *si_idx)); write_nop_instruction(new_spv, *si_idx); @@ -369,7 +412,10 @@ fn raw_image_fetch() { assert_eq!(traces.len(), 1); assert!(matches!( traces[0], - OpaqueLoadTrace::RawImage(RawImageOp::Fetch(4)) + OpaqueLoadTrace { + load_idx: 0, + next: OpaqueImageOp::RawImage(RawImageOp::Fetch(4)) + } )); } @@ -403,10 +449,13 @@ fn sampled_image_implicit_lod() { assert_eq!(traces.len(), 1); assert!(matches!( traces[0], - OpaqueLoadTrace::Sampled(SampledImageOp { - idx: 8, - next: SampledImageVariant::SampleImplicitLod(13), - }) + OpaqueLoadTrace { + load_idx: 0, + next: OpaqueImageOp::Sampled(SampledImageOp { + idx: 8, + next: SampledImageVariant::SampleImplicitLod(13), + }) + } )); } @@ -428,6 +477,9 @@ fn storage_image_write() { assert_eq!(traces.len(), 1); assert!(matches!( traces[0], - OpaqueLoadTrace::RawStorage(StorageTextureOp::Write(4)) + OpaqueLoadTrace { + load_idx: 0, + next: OpaqueImageOp::RawStorage(StorageTextureOp::Write(4)) + } )); } From fbb6c673a0598ef2aa6866c1a453f54c695c414b Mon Sep 17 00:00:00 2001 From: davnotdev Date: Fri, 22 May 2026 11:56:33 -0700 Subject: [PATCH 08/16] WIP separate rechaining and single arrayed sampler --- src/splitbindingarray.rs | 108 ++++++++++++-- src/splitbindingarray/rechain_instructions.rs | 134 ++++++++++++++++++ src/splitbindingarray/select_template.rs | 124 ++++------------ src/test.rs | 12 ++ src/test/splitbindingarray/compile.sh | 2 + .../sampler_binding_array.frag | 17 +++ .../sampler_binding_array.spv | Bin 0 -> 1548 bytes src/test/splitbindingarray/sampler_stub.frag | 17 +++ src/test/splitbindingarray/sampler_stub.spv | Bin 0 -> 1220 bytes src/util/opaque_trace.rs | 97 ++++++------- 10 files changed, 347 insertions(+), 164 deletions(-) create mode 100644 src/splitbindingarray/rechain_instructions.rs create mode 100644 src/test/splitbindingarray/sampler_binding_array.frag create mode 100644 src/test/splitbindingarray/sampler_binding_array.spv create mode 100644 src/test/splitbindingarray/sampler_stub.frag create mode 100644 src/test/splitbindingarray/sampler_stub.spv diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index 4d9a795..31d2f14 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -9,8 +9,10 @@ fn inc(ib: &mut u32) -> u32 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct OpaqueArrayType; +mod rechain_instructions; mod select_template; +use rechain_instructions::*; use select_template::*; /// Perform the operation on a `Vec`. @@ -68,6 +70,7 @@ pub fn splitbindingarray( let mut op_function_end_idxs = vec![]; let mut op_decorate_idxs = vec![]; let mut op_name_idxs = vec![]; + let mut op_sampled_image_idxs = vec![]; // 1. Find locations instructions we need let mut spv_idx = 0; @@ -97,6 +100,7 @@ pub fn splitbindingarray( SPV_INSTRUCTION_OP_FUNCTION_END => op_function_end_idxs.push(spv_idx), SPV_INSTRUCTION_OP_DECORATE => op_decorate_idxs.push(spv_idx), SPV_INSTRUCTION_OP_NAME => op_name_idxs.push(spv_idx), + SPV_INSTRUCTION_OP_SAMPLED_IMAGE => op_sampled_image_idxs.push(spv_idx), _ => {} } @@ -236,8 +240,7 @@ pub fn splitbindingarray( }); } - // 5. Replace OpAccessChain with selection function - for (ac_idx, v_idx, ta_idx, array_type) in op_access_chain_idxs + let access_idxs = op_access_chain_idxs .iter() .chain(op_in_bounds_access_chain_idxs.iter()) .filter_map(|&ac_idx| { @@ -250,7 +253,33 @@ pub fn splitbindingarray( }) .map(|(v_idx, ta_idx, array_type)| (ac_idx, v_idx, ta_idx, array_type)) }) - { + .collect::>(); + + // 5. Trace array samplers into a map + // Arrayed samplers turn our neat trace tree into a DAG. + // To keep things simple, we handle samplers separately. + // See `opaque_trace.rs` for details. + let mut arrayed_sampler_map = HashMap::new(); + for &(ac_idx, &v_idx, _, &array_type) in access_idxs.iter() { + let access_result_id = spv[ac_idx + 2]; + if let Some(OpaqueArrayType) = array_type { + for &load_idx in op_load_idxs.iter() { + let result_id = spv[load_idx + 2]; + let pointer_id = spv[load_idx + 3]; + if pointer_id == access_result_id { + for &sampled_image_idx in op_sampled_image_idxs.iter() { + let sampler_id = spv[sampled_image_idx + 4]; + if sampler_id == result_id { + arrayed_sampler_map.insert(sampled_image_idx, v_idx); + } + } + } + } + } + } + + // 6. Replace OpAccessChain with selection function + for &(ac_idx, v_idx, ta_idx, array_type) in access_idxs.iter() { let ac_word_count = hiword(spv[ac_idx]) as usize; new_spv[ac_idx..ac_idx + ac_word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); @@ -272,15 +301,54 @@ pub fn splitbindingarray( .collect::>(); let dependent_traces = trace_loaded_opaques(&spv, &load_idxs); for trace in dependent_traces { + let maybe_sampler_array_v_idx = match trace.next { + OpaqueImageOp::Sampled(sampled_image_op) => { + arrayed_sampler_map.get(&sampled_image_op.idx) + } + _ => None, + }; + let switch_instructions = reconstruct_opaque_trace_and_overwrite(&spv, &mut new_spv, &trace); + let rotate_image_sampler = matches!( + trace.next, + OpaqueImageOp::Sampled(SampledImageOp { + parent: SampledImageParent::Sampler, + .. + }) + ); + let builder = |ib: &mut u32, target_id: u32| { + if let Some(sampler_array_v_idx) = maybe_sampler_array_v_idx { + // Uh oh, we have a sampler array, we will need to nest this trace further. + // todo!() + } else { + // let (instructions, output) = rechain_instructions_with_target_id( + // ib, + // &switch_instructions, + // target_id, + // false, + // ); + // (instructions, output.map(|(_, id)| id)) + } + let (instructions, output) = rechain_instructions_with_target_id( + ib, + &switch_instructions, + target_id, + false, + rotate_image_sampler, + ); + (instructions, output.map(|(_, id)| id)) + }; + + let underlying_type_and_target_id = + get_last_instruction_result_type_and_id(&switch_instructions); let switch = select_template_spv( &mut instruction_bound, base_id, index_0_id, - &switch_instructions, length as usize, - false, + builder, + underlying_type_and_target_id, ); instruction_inserts.push(InstructionInsert { previous_spv_idx: trace.last_result_id(), @@ -344,13 +412,27 @@ pub fn splitbindingarray( new_spv[spv_idx..spv_idx + word_count] .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + + let builder = &|ib: &mut u32, target_id: u32| { + let (instructions, output) = rechain_instructions_with_target_id( + ib, + &new_instructions, + target_id, + flip_store_into, + false, + ); + (instructions, output.map(|(_, id)| id)) + }; + + let underlying_type_and_target_id = + get_last_instruction_result_type_and_id(&new_instructions); let switch = select_template_spv( &mut instruction_bound, base_id, index_0_id, - &new_instructions, length as usize, - flip_store_into, + builder, + underlying_type_and_target_id, ); instruction_inserts.push(InstructionInsert { previous_spv_idx: spv_idx, @@ -361,7 +443,7 @@ pub fn splitbindingarray( } } - // 6. Find OpDecorate / OpName to OpVariable + // 7. Find OpDecorate / OpName to OpVariable let unused_decorate_idxs = op_decorate_idxs .iter() .filter(|&idx| { @@ -385,7 +467,7 @@ pub fn splitbindingarray( .copied() .collect::>(); - // 7. Remove Instructions that have been Whited Out. + // 8. Remove Instructions that have been Whited Out. for &spv_idx in unused_decorate_idxs.iter().chain(unused_name_idxs.iter()) { let op = spv[spv_idx]; let word_count = hiword(op) as usize; @@ -393,7 +475,7 @@ pub fn splitbindingarray( new_spv[spv_idx..spv_idx + word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); } - // 8. OpDecorate + // 9. OpDecorate let DecorateOut { descriptor_sets_to_correct, } = util::decorate(DecorateIn { @@ -405,17 +487,17 @@ pub fn splitbindingarray( corrections, }); - // 9. Insert New Instructions + // 10. Insert New Instructions instruction_inserts.insert(0, types_header_insert); insert_new_instructions(&spv, &mut new_spv, &word_inserts, &instruction_inserts); - // 10. Correct OpDecorate Bindings + // 11. Correct OpDecorate Bindings util::correct_decorate(CorrectDecorateIn { new_spv: &mut new_spv, descriptor_sets_to_correct, }); prune_noops(&mut new_spv); - // 11. Write New Header and New Code + // 12. Write New Header and New Code Ok(fuse_final(spv_header, new_spv, instruction_bound)) } diff --git a/src/splitbindingarray/rechain_instructions.rs b/src/splitbindingarray/rechain_instructions.rs new file mode 100644 index 0000000..ebac86e --- /dev/null +++ b/src/splitbindingarray/rechain_instructions.rs @@ -0,0 +1,134 @@ +use super::*; + +// +// Take the any chain of instructions with the following form: +// OpSomething %result_type_id %result_id %input ... +// +// New temp variables are properly chained between instructions. +// +// The final instruction's `[idx+2]` is replaced with `%target_id` +// +// The final instruction can be a write operation. +// Write instructions are specially checked for because they follow a different convention. +// The following are considered valid write instructions: +// OpStore, OpCopyMemory, OpImageWrite +// +// `flip_store_into` specifically changes `%a = %result` to `%result = %a` +// `chain_sampler_over_image` specifically changes OpImageSampled to chain to the sampler. +// +pub fn rechain_instructions_with_target_id( + ib: &mut u32, + snippet: &[u32], + target_id: u32, + flip_store_into: bool, + rotate_image_sampler: bool, +) -> (Vec, Option<(u32, u32)>) { + let mut instruction_offsets = vec![]; + let mut idx = 0; + while idx < snippet.len() { + instruction_offsets.push(idx); + idx += hiword(snippet[idx]) as usize; + } + + let last_j = instruction_offsets.len() - 1; + let last_off = instruction_offsets[last_j]; + let returns_result = !matches!( + loword(snippet[last_off]), + SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_COPY_MEMORY | SPV_INSTRUCTION_OP_IMAGE_WRITE + ); + + let mut patched = snippet.to_vec(); + let mut current_source = target_id; + for (j, &off) in instruction_offsets.iter().enumerate() { + if j < last_j || returns_result { + let new_temp = inc(ib); + + if rotate_image_sampler && loword(patched[off]) == SPV_INSTRUCTION_OP_SAMPLED_IMAGE { + patched[off + 4] = current_source; + } else { + patched[off + 3] = current_source; + } + + patched[off + 2] = new_temp; + current_source = new_temp; + } else if flip_store_into { + patched[off + 2] = current_source; + } else { + patched[off + 1] = current_source; + } + } + + let last_offset = instruction_offsets[last_j]; + let underlying_type_and_target = (patched[last_offset + 1], patched[last_offset + 2]); + + ( + patched, + returns_result.then_some(underlying_type_and_target), + ) +} + +// Intended to run alongside the previous function with the same snippet properties. +// If the last instruction is not an expected store operation, return the final result type and id. +pub fn get_last_instruction_result_type_and_id(snippet: &[u32]) -> Option<(u32, u32)> { + let mut last_off = 0; + let mut idx = 0; + while idx < snippet.len() { + last_off = idx; + idx += hiword(snippet[idx]) as usize; + } + + let returns_result = !matches!( + loword(snippet[last_off]), + SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_COPY_MEMORY | SPV_INSTRUCTION_OP_IMAGE_WRITE + ); + + returns_result.then_some((snippet[last_off + 1], snippet[last_off + 2])) +} + +#[test] +fn two_chained_result_instructions() { + #[rustfmt::skip] + let snippet: &[u32] = &[ + encode_word(4, SPV_INSTRUCTION_OP_LOAD), 10, 1, 2, + encode_word(4, SPV_INSTRUCTION_OP_LOAD), 20, 3, 4, + ]; + let mut ib = 500u32; + let (out, result) = rechain_instructions_with_target_id(&mut ib, snippet, 100, false, false); + assert_eq!(out[2], 500); + assert_eq!(out[3], 100); + assert_eq!(out[6], 501); + assert_eq!(out[7], 500); + assert_eq!(result, Some((20, 501))); +} + +#[test] +fn terminal_store_no_flip() { + #[rustfmt::skip] + let snippet: &[u32] = &[ + encode_word(4, SPV_INSTRUCTION_OP_LOAD), 10, 1, 2, + encode_word(3, SPV_INSTRUCTION_OP_STORE), 3, 4, + ]; + let mut ib = 500u32; + let (out, result) = rechain_instructions_with_target_id(&mut ib, snippet, 100, false, false); + assert_eq!(out[2], 500); + assert_eq!(out[3], 100); + assert_eq!(out[5], 500); + assert_eq!(out[6], 4); + assert_eq!(result, None); +} + +#[test] +fn terminal_store_flip() { + #[rustfmt::skip] + let snippet: &[u32] = &[ + encode_word(4, SPV_INSTRUCTION_OP_LOAD), 10, 1, 2, + encode_word(3, SPV_INSTRUCTION_OP_STORE), 3, 4, + ]; + let mut ib = 500u32; + let (out, result) = rechain_instructions_with_target_id(&mut ib, snippet, 100, true, false); + assert_eq!(out[2], 500); + assert_eq!(out[3], 100); + assert_eq!(out[5], 3); + assert_eq!(out[6], 500); + assert_eq!(result, None); +} diff --git a/src/splitbindingarray/select_template.rs b/src/splitbindingarray/select_template.rs index 8266ed0..b0f8cfb 100644 --- a/src/splitbindingarray/select_template.rs +++ b/src/splitbindingarray/select_template.rs @@ -1,23 +1,12 @@ use super::*; -// Take the any chain of instructions with the following form: -// OpSomething %result_type_id %result_id %input ... -// -// The instruction's source, `[idx+2]` is replaced with `%base_id+N` -// and duplicated for each case of the index, see template below. -// -// Write instructions are specially checked for because they follow a different convention: -// OpStore, OpCopyMemory, OpImageWrite -// -// `flip_store_into` specifically changes `%a = %result` to `%result = %a` -// -pub(super) fn select_template_spv( +pub(super) fn select_template_spv (Vec, Option)>( ib: &mut u32, base_id: u32, index_id: u32, - switch_instructions: &[u32], length: usize, - flip_store_into: bool, + mut instruction_builder: F, + result_type_and_id: Option<(u32, u32)>, ) -> Vec { // // TODO: You can probably decrease the instruction count with OpPhi or OpSelect. @@ -25,92 +14,30 @@ pub(super) fn select_template_spv( // OpSelectionMerge %merge None // OpSwitch %index_id %default %merge 0 %case_0 1 %case_1 ... N %case_N // %case_0 = OpLabel - // %temp_0 = {INSTRUCTION}(%base_id+0) - // ... + // %temp_0 = {instruction_builder(%base_id+0)} // OpBranch %merge // %case_1 = OpLabel - // %temp_1 = {INSTRUCTION}(%base_id+1) - // ... + // %temp_1 = {instruction_builder(%base_id+1)} // OpBranch %merge + // // ... + // // %case_N = OpLabel - // %temp_N = {INSTRUCTION}(%base_id+N) - // ... + // %temp_N = {instruction_builder(%base_id+N)} // OpBranch %merge - // %default = OpLabel - // %temp_0 = {INSTRUCTION}(%base_id+0) - // ... - // %merge = OpLabel + // %default = OpLabel + // %temp_def = {instruction_builder(%base_id+0)} + // %merge = OpLabel // - // ; Only if there will be a result value. - // %target_id = OpPhi %underlying_type_id %temp_0 %case_0 %temp_1 %case_1 ... %temp_N %case_N + // ; Only if there will be a result value. + // %target_id = OpPhi %underlying_type_id %temp_0 %case_0 %temp_1 %case_1 ... %temp_N %case_N %temp_def %default // - let mut instruction_offsets = vec![]; - let mut idx = 0; - while idx < switch_instructions.len() { - instruction_offsets.push(idx); - idx += hiword(switch_instructions[idx]) as usize; - } - - let last_j = instruction_offsets.len() - 1; - let last_off = instruction_offsets[last_j]; - - let returns_result = !matches!( - loword(switch_instructions[last_off]), - SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_IMAGE_WRITE | SPV_INSTRUCTION_OP_COPY_MEMORY - ); - let case_labels = (0..length).map(|_| inc(ib)).collect::>(); let default_label = inc(ib); let merge_label = inc(ib); - let case_temps: Vec> = (0..length) - .map(|_| { - instruction_offsets - .iter() - .enumerate() - .map(|(j, _)| { - if j < last_j || returns_result { - inc(ib) - } else { - 0 - } - }) - .collect() - }) - .collect(); - let default_temps: Vec = instruction_offsets - .iter() - .enumerate() - .map(|(j, _)| { - if j < last_j || returns_result { - inc(ib) - } else { - 0 - } - }) - .collect(); - - let make_instructions = |temps: &[u32], base: u32| -> Vec { - let mut patched = switch_instructions.to_vec(); - let mut current_source = base; - for (j, &off) in instruction_offsets.iter().enumerate() { - if j < last_j || returns_result { - patched[off + 2] = temps[j]; - patched[off + 3] = current_source; - current_source = temps[j]; - } else if flip_store_into { - patched[off + 2] = current_source; - } else { - patched[off + 1] = current_source; - } - } - patched - }; - let mut spv = vec![]; - spv.extend_from_slice(&[ encode_word(3, SPV_INSTRUCTION_OP_SELECTION_MERGE), merge_label, @@ -124,35 +51,40 @@ pub(super) fn select_template_spv( spv.push(case_label); } + let mut output_ids = vec![]; for (i, &case_label) in case_labels.iter().enumerate() { spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_LABEL), case_label]); - spv.extend_from_slice(&make_instructions(&case_temps[i], base_id + i as u32)); + let (instructions, maybe_output_id) = instruction_builder(ib, base_id + i as u32); + spv.extend_from_slice(&instructions); + if let Some(output_id) = maybe_output_id { + output_ids.push(output_id); + } spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_BRANCH), merge_label]); } - spv.extend_from_slice(&[encode_word(2, SPV_INSTRUCTION_OP_LABEL), default_label]); - spv.extend_from_slice(&make_instructions(&default_temps, base_id)); + let (instructions, default_output_id) = instruction_builder(ib, base_id); + spv.extend_from_slice(&instructions); spv.extend_from_slice(&[ encode_word(2, SPV_INSTRUCTION_OP_BRANCH), merge_label, encode_word(2, SPV_INSTRUCTION_OP_LABEL), merge_label, ]); - - if returns_result { - let result_type_id = switch_instructions[last_off + 1]; - let result_id = switch_instructions[last_off + 2]; + if let Some((result_type_id, target_id)) = result_type_and_id { + assert!(output_ids.len() == length); spv.push(encode_word( 3 + 2 * (length as u16 + 1), SPV_INSTRUCTION_OP_PHI, )); spv.push(result_type_id); - spv.push(result_id); + spv.push(target_id); for (i, &case_label) in case_labels.iter().enumerate() { - spv.push(case_temps[i][last_j]); + spv.push(output_ids[i]); spv.push(case_label); } - spv.push(default_temps[last_j]); + spv.push( + default_output_id.expect("default block must produce output when result is expected"), + ); spv.push(default_label); } diff --git a/src/test.rs b/src/test.rs index 86bef1c..dd727c7 100644 --- a/src/test.rs +++ b/src/test.rs @@ -276,3 +276,15 @@ test_with_spv_and_fn![ "./test/splitbindingarray/texture_binding_array.spv", splitbindingarray ]; +test_with_spv_and_fn![ + splitbinding_splitbinding_sampler_binding_array, + DO_ALL, + "./test/splitbindingarray/sampler_binding_array.spv", + splitbindingarray +]; +test_with_spv_and_fn![ + splitbinding_splitbinding_sampler_stub, + DO_ALL, + "./test/splitbindingarray/sampler_stub.spv", + splitbindingarray +]; diff --git a/src/test/splitbindingarray/compile.sh b/src/test/splitbindingarray/compile.sh index 550cae3..ad854a1 100755 --- a/src/test/splitbindingarray/compile.sh +++ b/src/test/splitbindingarray/compile.sh @@ -3,4 +3,6 @@ set -e glslc -O0 buffer_binding_array.frag -o buffer_binding_array.spv glslc -O0 storage_binding_array.frag -o storage_binding_array.spv glslc -O0 texture_binding_array.frag -o texture_binding_array.spv +glslc -O0 sampler_binding_array.frag -o sampler_binding_array.spv glslc -O0 nested_texture_binding_array.frag -o nested_texture_binding_array.spv +glslc -O0 sampler_stub.frag -o sampler_stub.spv diff --git a/src/test/splitbindingarray/sampler_binding_array.frag b/src/test/splitbindingarray/sampler_binding_array.frag new file mode 100644 index 0000000..a910fb5 --- /dev/null +++ b/src/test/splitbindingarray/sampler_binding_array.frag @@ -0,0 +1,17 @@ +#version 440 + +#define MAX_TEXTURES 8 + +layout(location = 0) out vec4 o_color; + +layout(set = 0, binding = 0) uniform texture2D u_textures[MAX_TEXTURES]; +layout(set = 0, binding = 1) uniform sampler u_samplers[MAX_TEXTURES + 1]; + +void main() { + o_color = vec4(0.0); + for (int i = 0, j = 1; i < MAX_TEXTURES; i++, j++) { + o_color += texture(sampler2D(u_textures[i], u_samplers[j]), vec2(0.0, 0.0)); + o_color += texture(sampler2D(u_textures[0], u_samplers[i]), vec2(0.0, 0.0)); + } +} + diff --git a/src/test/splitbindingarray/sampler_binding_array.spv b/src/test/splitbindingarray/sampler_binding_array.spv new file mode 100644 index 0000000000000000000000000000000000000000..daa11bd324c273354a5a89ac7bf5218da784c13d GIT binary patch literal 1548 zcmY+C$!-%-5JjJpIAkCbnaBXN33Gy(k}!k>f`q)Wlm!wjTanWXBWIB9AlShtvE$1S zAR%#1yPss9bi1l<-B(>*)q?|T7eXk7@h}nIgx;A6126$@IJdR>yLz?LJ*sYQJybCn z26LgBDKtY^aeUZ3F)<3J(a2F8Z;*;1{NTb+7z?#}T01zX)!(L#v$M3*{nE4ma^Aax^~hV-*d2MyulL;i&I*&S68jE!fHkk(CGSobf%U%Ai91^^ICuF6jqxn7mUs2z-3q>U_qprL*FXZ~)i|3QKtA5N zhi}i`ah@B%8Oy72ANPU$do)$-17IEb4{*-pF_3r0-jjO*oYS{LvsrLz;|%3uzgzeT zaF(&(ZG8FI?^FEvjrRKt*spx-_j$p`eqRtCT?@QwTes_uG)W&}0+?{)L w{@U2v9=^SO<$K-bDIsS zfP}>Njoo4@da0_{ud2JNN3P5tT?%0?tc2C@EKF@P%)ax1YBA-Tp9C*>jCN2UY%V;wR}i`!b#kv(c10 zEx6J7#h{xF;hM7^YBh7?T9N(FZ z3ICVyPFMc*Ecae=56IOn1Feoq-3pK^V+ZXOxCD4Md#~cv%iBx84z!r>5_^ViPP>QS zXR7;n_q9*iV(wlCEF`lFgg8doSOp&NpJ-0CA3SCQsnzz)Li0eJjm3`2|zO zySMZ8n{&o*S@qp~8)JEA{TuxT-a7qH>aOCqfadx(-gW&O)_tcPV2|(Y=DS>*alY3d zH0GPYUe+4N+FQidzN0q5*MPH@SL2NRX60ke9pYHy4DSKwDzC;jxDVt%($~4xfqmpZ r!8wOVK)wOIoA>m5`nZ=rD(>}-8t?iP$T{~oJGt`gyystD Vec { + panic!("DAG node: OpSampledImage at {si_idx} has both image and sampler from tracked loads") + } + (Some(load_idx), None) => { + Some((spv[si_idx + 2], si_idx, load_idx, SampledImageParent::Image)) + } + (None, Some(load_idx)) => { + Some((spv[si_idx + 2], si_idx, load_idx, SampledImageParent::Sampler)) + } + (None, None) => None, + } }) - .map(|(idx, load_idx)| (spv[idx + 2], idx, load_idx)) .collect::>(); for &(instruction, idx) in sampled_image_op_idxs.iter() { - let Some(&(_, si_idx, load_idx)) = - sampled_image_entries.iter().find(|(result_id, _, _)| { + let Some(&(_, si_idx, load_idx, parent)) = + sampled_image_entries.iter().find(|(result_id, _, _, _)| { let loaded_image_id = spv[idx + 3]; *result_id == loaded_image_id }) @@ -302,14 +318,15 @@ pub fn trace_loaded_opaques(spv: &[u32], load_idxs: &[usize]) -> Vec Vec { + OpaqueImageOp::Sampled(SampledImageOp { idx: si_idx, next, .. }) => { out.extend_from_slice(take_instruction(spv, *si_idx)); write_nop_instruction(new_spv, *si_idx); @@ -395,18 +413,10 @@ pub fn reconstruct_opaque_trace_and_overwrite( #[test] fn raw_image_fetch() { + #[rustfmt::skip] let spv: &[u32] = &[ - // %20 = OpLoad %10 %30 - encode_word(4, SPV_INSTRUCTION_OP_LOAD), - 10, - 20, - 30, - // %21 = OpImageFetch %11 %20 %40 - encode_word(5, SPV_INSTRUCTION_OP_IMAGE_FETCH), - 11, - 21, - 20, - 40, + encode_word(4, SPV_INSTRUCTION_OP_LOAD), 10, 20, 30, + encode_word(5, SPV_INSTRUCTION_OP_IMAGE_FETCH), 11, 21, 20, 40, ]; let traces = trace_loaded_opaques(spv, &[0]); assert_eq!(traces.len(), 1); @@ -421,29 +431,12 @@ fn raw_image_fetch() { #[test] fn sampled_image_implicit_lod() { + #[rustfmt::skip] let spv: &[u32] = &[ - // %20 = OpLoad %10 %30 - encode_word(4, SPV_INSTRUCTION_OP_LOAD), - 10, - 20, - 30, - // %21 = OpLoad %11 %31 - encode_word(4, SPV_INSTRUCTION_OP_LOAD), - 11, - 21, - 31, - // %22 = OpSampledImage %12 %20 %21 - encode_word(5, SPV_INSTRUCTION_OP_SAMPLED_IMAGE), - 12, - 22, - 20, - 21, - // %23 = OpImageSampleImplicitLod %13 %22 %40 - encode_word(5, SPV_INSTRUCTION_OP_IMAGE_SAMPLE_IMPLICIT_LOD), - 13, - 23, - 22, - 40, + encode_word(4, SPV_INSTRUCTION_OP_LOAD), 10, 20, 30, + encode_word(4, SPV_INSTRUCTION_OP_LOAD), 11, 21, 31, + encode_word(5, SPV_INSTRUCTION_OP_SAMPLED_IMAGE), 12, 22, 20, 21, + encode_word(5, SPV_INSTRUCTION_OP_IMAGE_SAMPLE_IMPLICIT_LOD), 13, 23, 22, 40, ]; let traces = trace_loaded_opaques(spv, &[0]); assert_eq!(traces.len(), 1); @@ -453,6 +446,7 @@ fn sampled_image_implicit_lod() { load_idx: 0, next: OpaqueImageOp::Sampled(SampledImageOp { idx: 8, + parent: SampledImageParent::Image, next: SampledImageVariant::SampleImplicitLod(13), }) } @@ -461,17 +455,10 @@ fn sampled_image_implicit_lod() { #[test] fn storage_image_write() { + #[rustfmt::skip] let spv: &[u32] = &[ - // %20 = OpLoad %10 %30 - encode_word(4, SPV_INSTRUCTION_OP_LOAD), - 10, - 20, - 30, - // OpImageWrite %20 %40 %50 - encode_word(4, SPV_INSTRUCTION_OP_IMAGE_WRITE), - 20, - 40, - 50, + encode_word(4, SPV_INSTRUCTION_OP_LOAD), 10, 20, 30, + encode_word(4, SPV_INSTRUCTION_OP_IMAGE_WRITE), 20, 40, 50, ]; let traces = trace_loaded_opaques(spv, &[0]); assert_eq!(traces.len(), 1); From 042372f950a5c44c197f3f7fda113996ccd69719 Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 27 May 2026 11:10:01 -0700 Subject: [PATCH 09/16] Double case: arrayed sampler and arrayed texture --- src/splitbindingarray.rs | 167 ++++++++++++++++-- src/splitbindingarray/rechain_instructions.rs | 8 +- src/test.rs | 6 + src/util.rs | 2 + src/util/instruction.rs | 13 ++ 5 files changed, 170 insertions(+), 26 deletions(-) create mode 100644 src/util/instruction.rs diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index 31d2f14..f619061 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -260,7 +260,7 @@ pub fn splitbindingarray( // To keep things simple, we handle samplers separately. // See `opaque_trace.rs` for details. let mut arrayed_sampler_map = HashMap::new(); - for &(ac_idx, &v_idx, _, &array_type) in access_idxs.iter() { + for &(ac_idx, &v_idx, ta_idx, &array_type) in access_idxs.iter() { let access_result_id = spv[ac_idx + 2]; if let Some(OpaqueArrayType) = array_type { for &load_idx in op_load_idxs.iter() { @@ -270,7 +270,7 @@ pub fn splitbindingarray( for &sampled_image_idx in op_sampled_image_idxs.iter() { let sampler_id = spv[sampled_image_idx + 4]; if sampler_id == result_id { - arrayed_sampler_map.insert(sampled_image_idx, v_idx); + arrayed_sampler_map.insert(sampled_image_idx, (ac_idx, v_idx, ta_idx)); } } } @@ -291,6 +291,27 @@ pub fn splitbindingarray( let base_id = new_variables_map[v_idx]; if let Some(OpaqueArrayType) = *array_type { + // When both a texture array and a sampler array feed the same OpSampledImage, + // the texture AC's processing already generates the correct nested switch + // (outer = texture index, inner = sampler index via `maybe_sampler_array_data`). + // + // Detect this by checking whether this AC is already stored as the sampler + // dimension in `arrayed_sampler_map`. If so, just NOP its dependent loads + // (they reference the now-undefined AC result) and skip switch generation. + let is_inner_sampler_ac = arrayed_sampler_map + .values() + .any(|&(map_ac_idx, _, _)| map_ac_idx == ac_idx); + if is_inner_sampler_ac { + for &load_idx in op_load_idxs.iter() { + if spv[load_idx + 3] == old_result_id { + let wc = hiword(spv[load_idx]) as usize; + new_spv[load_idx..load_idx + wc] + .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + } + } + continue; + } + let load_idxs = op_load_idxs .iter() .filter(|&idx| { @@ -301,7 +322,7 @@ pub fn splitbindingarray( .collect::>(); let dependent_traces = trace_loaded_opaques(&spv, &load_idxs); for trace in dependent_traces { - let maybe_sampler_array_v_idx = match trace.next { + let maybe_sampler_array_data = match trace.next { OpaqueImageOp::Sampled(sampled_image_op) => { arrayed_sampler_map.get(&sampled_image_op.idx) } @@ -310,6 +331,8 @@ pub fn splitbindingarray( let switch_instructions = reconstruct_opaque_trace_and_overwrite(&spv, &mut new_spv, &trace); + let underlying_type_and_target_id = + get_last_instruction_result_type_and_id(&switch_instructions); let rotate_image_sampler = matches!( trace.next, OpaqueImageOp::Sampled(SampledImageOp { @@ -317,19 +340,7 @@ pub fn splitbindingarray( .. }) ); - let builder = |ib: &mut u32, target_id: u32| { - if let Some(sampler_array_v_idx) = maybe_sampler_array_v_idx { - // Uh oh, we have a sampler array, we will need to nest this trace further. - // todo!() - } else { - // let (instructions, output) = rechain_instructions_with_target_id( - // ib, - // &switch_instructions, - // target_id, - // false, - // ); - // (instructions, output.map(|(_, id)| id)) - } + let rechain_instructions = |ib: &mut u32, target_id: u32| { let (instructions, output) = rechain_instructions_with_target_id( ib, &switch_instructions, @@ -339,10 +350,114 @@ pub fn splitbindingarray( ); (instructions, output.map(|(_, id)| id)) }; + // Track inner merge labels per outer case so we can fix the outer phi after select_template_spv runs. + // `select_template_spv` puts the outer case labels in the phi, but with a nested inner switch the actual predecessor + // of the outer merge is the inner merge block, not the outer case block. + let mut inner_merge_labels: Vec = vec![]; - let underlying_type_and_target_id = - get_last_instruction_result_type_and_id(&switch_instructions); - let switch = select_template_spv( + let builder = |ib: &mut u32, target_id: u32| { + if let Some((sampler_array_ac_idx, sampler_array_v_idx, sampler_array_ta_idx)) = + maybe_sampler_array_data + { + let sampler_base_id = new_variables_map[sampler_array_v_idx]; + let sampler_index_0_id = spv[sampler_array_ac_idx + 4]; + let sampler_length = length_map[sampler_array_ta_idx] as usize; + + // Rechain only the image load for this outer case. + // switch_instructions = [image_load, OpSampledImage, ...] + // target_id is the split image variable for this outer case. + let image_load_wc = hiword(switch_instructions[0]) as usize; + let (image_load_instrs, image_out) = rechain_instructions_with_target_id( + ib, + &switch_instructions[..image_load_wc], + target_id, + false, + false, + ); + let (_, new_image_id) = + image_out.expect("image load must produce a result"); + + // Locate OpSampledImage and any instructions that follow it. + let si_wc = hiword(switch_instructions[image_load_wc]) as usize; + let after_si = &switch_instructions[image_load_wc + si_wc..]; + let sampler_type_id = spv[op_type_sampler_idxs[0] + 1]; + + // Inner builder: per sampler variable j, emit sampler load + OpSampledImage + trailing instructions. + // The image load is placed before the inner switch. + let inner_builder = |ib: &mut u32, inner_target_id: u32| { + let mut instrs = vec![]; + + let new_sampler_result = inc(ib); + instrs.extend_from_slice(&[ + encode_word(4, SPV_INSTRUCTION_OP_LOAD), + sampler_type_id, + new_sampler_result, + inner_target_id, + ]); + + let new_si_result = inc(ib); + let mut si_patched = + switch_instructions[image_load_wc..image_load_wc + si_wc].to_vec(); + si_patched[2] = new_si_result; + si_patched[3] = new_image_id; + si_patched[4] = new_sampler_result; + instrs.extend_from_slice(&si_patched); + + if !after_si.is_empty() { + let (chained, output) = rechain_instructions_with_target_id( + ib, + after_si, + new_si_result, + false, + false, + ); + instrs.extend_from_slice(&chained); + return (instrs, output.map(|(_, id)| id)); + } + (instrs, Some(new_si_result)) + }; + + let mut inner_switch = select_template_spv( + ib, + sampler_base_id, + sampler_index_0_id, + sampler_length, + inner_builder, + underlying_type_and_target_id, + ); + + // Find the inner merge label (last OpLabel before the inner phi). + let phi_idx = get_last_instruction_index(&inner_switch); + { + let mut idx = 0; + let mut label = 0u32; + while idx < phi_idx { + if loword(inner_switch[idx]) == SPV_INSTRUCTION_OP_LABEL { + label = inner_switch[idx + 1]; + } + idx += hiword(inner_switch[idx]) as usize; + } + inner_merge_labels.push(label); + } + + // Patch the inner phi's result id to a fresh id for the outer phi. + let output_id = (loword(inner_switch[phi_idx]) == SPV_INSTRUCTION_OP_PHI) + .then(|| { + let new_id = inc(ib); + inner_switch[phi_idx + 2] = new_id; + new_id + }); + + // Emit: image load once for this outer case, then the inner switch. + let mut result = image_load_instrs; + result.extend_from_slice(&inner_switch); + (result, output_id) + } else { + rechain_instructions(ib, target_id) + } + }; + + let mut switch = select_template_spv( &mut instruction_bound, base_id, index_0_id, @@ -350,6 +465,20 @@ pub fn splitbindingarray( builder, underlying_type_and_target_id, ); + + // Patch the outer phi's predecessor labels. + // select_template_spv filled them with the outer case labels, + // but each outer case now ends at its inner merge block, not at the outer case label. + // phi layout: [opword, type, result_id, val0, pred0, val1, pred1, ...] + if !inner_merge_labels.is_empty() { + let phi_idx = get_last_instruction_index(&switch); + if loword(switch[phi_idx]) == SPV_INSTRUCTION_OP_PHI { + for (i, &label) in inner_merge_labels.iter().enumerate() { + switch[phi_idx + 4 + 2 * i] = label; + } + } + } + instruction_inserts.push(InstructionInsert { previous_spv_idx: trace.last_result_id(), instruction: switch, diff --git a/src/splitbindingarray/rechain_instructions.rs b/src/splitbindingarray/rechain_instructions.rs index ebac86e..d1af9db 100644 --- a/src/splitbindingarray/rechain_instructions.rs +++ b/src/splitbindingarray/rechain_instructions.rs @@ -70,13 +70,7 @@ pub fn rechain_instructions_with_target_id( // Intended to run alongside the previous function with the same snippet properties. // If the last instruction is not an expected store operation, return the final result type and id. pub fn get_last_instruction_result_type_and_id(snippet: &[u32]) -> Option<(u32, u32)> { - let mut last_off = 0; - let mut idx = 0; - while idx < snippet.len() { - last_off = idx; - idx += hiword(snippet[idx]) as usize; - } - + let last_off = get_last_instruction_index(snippet); let returns_result = !matches!( loword(snippet[last_off]), SPV_INSTRUCTION_OP_STORE | SPV_INSTRUCTION_OP_COPY_MEMORY | SPV_INSTRUCTION_OP_IMAGE_WRITE diff --git a/src/test.rs b/src/test.rs index dd727c7..6348867 100644 --- a/src/test.rs +++ b/src/test.rs @@ -276,6 +276,12 @@ test_with_spv_and_fn![ "./test/splitbindingarray/texture_binding_array.spv", splitbindingarray ]; +test_with_spv_and_fn![ + splitbinding_splitbinding_nested_texture_binding_array, + DO_ALL, + "./test/splitbindingarray/nested_texture_binding_array.spv", + splitbindingarray +]; test_with_spv_and_fn![ splitbinding_splitbinding_sampler_binding_array, DO_ALL, diff --git a/src/util.rs b/src/util.rs index 10d2dcd..403ad5f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,6 +4,7 @@ mod correct_decorate; mod decorate; mod ensure; mod function; +mod instruction; mod opaque_trace; mod pointer; @@ -11,6 +12,7 @@ pub use correct_decorate::*; pub use decorate::*; pub use ensure::*; pub use function::*; +pub use instruction::*; pub use opaque_trace::*; pub use pointer::*; diff --git a/src/util/instruction.rs b/src/util/instruction.rs new file mode 100644 index 0000000..7c9afd5 --- /dev/null +++ b/src/util/instruction.rs @@ -0,0 +1,13 @@ +use super::*; + +// TODO: Implement `map_spirv` which iterates through a &[u32] of instructions. + +pub fn get_last_instruction_index(instructions: &[u32]) -> usize { + let mut last_off = 0; + let mut idx = 0; + while idx < instructions.len() { + last_off = idx; + idx += hiword(instructions[idx]) as usize; + } + last_off +} From 174561a6254af212cc3f70f50f6dba172d8d63e2 Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 27 May 2026 12:00:25 -0700 Subject: [PATCH 10/16] We really meant vfp not v --- src/splitbindingarray.rs | 50 ++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index f619061..18ddc45 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -118,7 +118,9 @@ pub fn splitbindingarray( } } - // 2. OpTypeArray -> OpTypePointer -> OpVariable + // 2. OpTypeArray -> OpTypePointer + // -> OpVariable + // -> OpFunctionParameter let array_tp_ta_idxs = op_type_pointer_idxs .iter() .filter_map(|&tp_idx| { @@ -150,22 +152,25 @@ pub fn splitbindingarray( }) .collect::>(); - let array_v_ta_idxs = op_variable_idxs + // Contains ((OpVariable or OpFunctionParameter), OpTypePointer, Option) + // OpVariable is a subtype of OpFunctionParameter over the first three words. + let array_vfp_ta_idxs = op_variable_idxs .iter() - .filter_map(|&v_idx| { - let variable_type_id = spv[v_idx + 1]; + .chain(op_function_parameter_idxs.iter()) + .filter_map(|&vfp_idx| { + let variable_type_id = spv[vfp_idx + 1]; array_tp_ta_idxs .iter() .find(|&(tp_idx, _, _)| { let tp_res_id = spv[tp_idx + 1]; tp_res_id == variable_type_id }) - .map(|&(_, ta_idx, array_type)| (v_idx, ta_idx, array_type)) + .map(|&(_, ta_idx, array_type)| (vfp_idx, ta_idx, array_type)) }) .collect::>(); // 3. Build mapping of lengths - let length_map = array_v_ta_idxs + let length_map = array_vfp_ta_idxs .iter() .map(|(_, ta_idx, _)| { let length_id = spv[ta_idx + 3]; @@ -187,8 +192,8 @@ pub fn splitbindingarray( let mut new_variables_map = HashMap::new(); let mut affected_decorations = vec![]; - for &(v_idx, ta_idx, array_type) in array_v_ta_idxs.iter() { - new_spv[v_idx..v_idx + hiword(spv[v_idx]) as usize] + for &(vfp_idx, ta_idx, array_type) in array_vfp_ta_idxs.iter() { + new_spv[vfp_idx..vfp_idx + hiword(spv[vfp_idx]) as usize] .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); let mut new_instruction = vec![]; @@ -229,9 +234,9 @@ pub fn splitbindingarray( // instruction: new_instruction, // }); types_header_insert.instruction.append(&mut new_instruction); - new_variables_map.insert(v_idx, base_id); + new_variables_map.insert(vfp_idx, base_id); - let old_result_id = spv[v_idx + 2]; + let old_result_id = spv[vfp_idx + 2]; let new_ids = (base_id..base_id + length).collect::>(); affected_decorations.push(AffectedDecoration { original_res_id: old_result_id, @@ -245,13 +250,13 @@ pub fn splitbindingarray( .chain(op_in_bounds_access_chain_idxs.iter()) .filter_map(|&ac_idx| { let base_id = spv[ac_idx + 3]; - array_v_ta_idxs + array_vfp_ta_idxs .iter() - .find(|&(v_idx, _, _)| { - let result_id = spv[*v_idx + 2]; + .find(|&(vfp_idx, _, _)| { + let result_id = spv[*vfp_idx + 2]; result_id == base_id }) - .map(|(v_idx, ta_idx, array_type)| (ac_idx, v_idx, ta_idx, array_type)) + .map(|(vfp_idx, ta_idx, array_type)| (ac_idx, vfp_idx, ta_idx, array_type)) }) .collect::>(); @@ -260,7 +265,7 @@ pub fn splitbindingarray( // To keep things simple, we handle samplers separately. // See `opaque_trace.rs` for details. let mut arrayed_sampler_map = HashMap::new(); - for &(ac_idx, &v_idx, ta_idx, &array_type) in access_idxs.iter() { + for &(ac_idx, &vfp_idx, ta_idx, &array_type) in access_idxs.iter() { let access_result_id = spv[ac_idx + 2]; if let Some(OpaqueArrayType) = array_type { for &load_idx in op_load_idxs.iter() { @@ -270,7 +275,8 @@ pub fn splitbindingarray( for &sampled_image_idx in op_sampled_image_idxs.iter() { let sampler_id = spv[sampled_image_idx + 4]; if sampler_id == result_id { - arrayed_sampler_map.insert(sampled_image_idx, (ac_idx, v_idx, ta_idx)); + arrayed_sampler_map + .insert(sampled_image_idx, (ac_idx, vfp_idx, ta_idx)); } } } @@ -279,7 +285,7 @@ pub fn splitbindingarray( } // 6. Replace OpAccessChain with selection function - for &(ac_idx, v_idx, ta_idx, array_type) in access_idxs.iter() { + for &(ac_idx, vfp_idx, ta_idx, array_type) in access_idxs.iter() { let ac_word_count = hiword(spv[ac_idx]) as usize; new_spv[ac_idx..ac_idx + ac_word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); @@ -288,7 +294,7 @@ pub fn splitbindingarray( let length = length_map[&ta_idx]; - let base_id = new_variables_map[v_idx]; + let base_id = new_variables_map[vfp_idx]; if let Some(OpaqueArrayType) = *array_type { // When both a texture array and a sampler array feed the same OpSampledImage, @@ -577,8 +583,8 @@ pub fn splitbindingarray( .iter() .filter(|&idx| { let target = spv[idx + 1]; - new_variables_map.iter().any(|(v_idx, _)| { - let result_id = spv[v_idx + 2]; + new_variables_map.iter().any(|(vfp_idx, _)| { + let result_id = spv[vfp_idx + 2]; target == result_id }) }) @@ -588,8 +594,8 @@ pub fn splitbindingarray( .iter() .filter(|&idx| { let target = spv[idx + 1]; - new_variables_map.iter().any(|(v_idx, _)| { - let result_id = spv[v_idx + 2]; + new_variables_map.iter().any(|(vfp_idx, _)| { + let result_id = spv[vfp_idx + 2]; target == result_id }) }) From b7ba904903f81660921fcff45b4d3b8c241b8186 Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 27 May 2026 13:50:32 -0700 Subject: [PATCH 11/16] Implement nested function binding array handling --- src/splitbindingarray.rs | 198 +++++++++++++++++++++++++++++++-------- 1 file changed, 157 insertions(+), 41 deletions(-) diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index 18ddc45..350c0fd 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -189,21 +189,23 @@ pub fn splitbindingarray( previous_spv_idx: types_header_position.unwrap(), instruction: vec![], }; - let mut new_variables_map = HashMap::new(); + let mut new_vfp_map = HashMap::new(); + let mut function_type_changes = HashMap::new(); let mut affected_decorations = vec![]; for &(vfp_idx, ta_idx, array_type) in array_vfp_ta_idxs.iter() { new_spv[vfp_idx..vfp_idx + hiword(spv[vfp_idx]) as usize] .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); - let mut new_instruction = vec![]; + let mut new_type_instructions = vec![]; + let instruction = loword(spv[vfp_idx]); let underlying_type_id = spv[ta_idx + 2]; let type_pointer_id = ensure_type_pointer( &spv, &op_type_pointer_idxs, &mut instruction_bound, - &mut new_instruction, + &mut new_type_instructions, match array_type { Some(OpaqueArrayType) => SPV_STORAGE_CLASS_UNIFORM_CONSTANT, _ => SPV_STORAGE_CLASS_UNIFORM, @@ -216,33 +218,101 @@ pub fn splitbindingarray( let base_id = instruction_bound; instruction_bound += length; - for i in 0..length { - new_instruction.append(&mut vec![ - encode_word(4, SPV_INSTRUCTION_OP_VARIABLE), - type_pointer_id, - base_id + i, - match array_type { - Some(OpaqueArrayType) => SPV_STORAGE_CLASS_UNIFORM_CONSTANT, - _ => SPV_STORAGE_CLASS_UNIFORM, - }, - ]); + match instruction { + SPV_INSTRUCTION_OP_VARIABLE => { + for i in 0..length { + new_type_instructions.append(&mut vec![ + encode_word(4, SPV_INSTRUCTION_OP_VARIABLE), + type_pointer_id, + base_id + i, + match array_type { + Some(OpaqueArrayType) => SPV_STORAGE_CLASS_UNIFORM_CONSTANT, + _ => SPV_STORAGE_CLASS_UNIFORM, + }, + ]); + } + // Ordering issues with this, let's keep it after all other type pointers. + // + // instruction_inserts.push(InstructionInsert { + // previous_spv_idx: v_idx, + // instruction: new_instruction, + // }); + types_header_insert + .instruction + .append(&mut new_type_instructions); + let old_result_id = spv[vfp_idx + 2]; + let new_ids = (base_id..base_id + length).collect::>(); + affected_decorations.push(AffectedDecoration { + original_res_id: old_result_id, + new_res_ids: new_ids, + correction_type: CorrectionType::SplitBindingArray(length), + }); + } + SPV_INSTRUCTION_OP_FUNCTION_PARAMETER => { + let mut new_param_instructions = vec![]; + for i in 0..length { + new_param_instructions.append(&mut vec![ + encode_word(3, SPV_INSTRUCTION_OP_FUNCTION_PARAMETER), + type_pointer_id, + base_id + i, + ]); + } + instruction_inserts.push(InstructionInsert { + previous_spv_idx: vfp_idx, + instruction: new_param_instructions, + }); + + let entry = get_function_from_parameter(&spv, vfp_idx); + let function_type_id = spv[entry.function_idx + 4]; + + // `entry.parameter_instruction_idx` is the 0-based ordinal of the parameter + // within the function; step 5 compares it against the loop variable `i`. + function_type_changes + .entry(function_type_id) + .or_insert(vec![]) + .push((entry.parameter_instruction_idx, type_pointer_id, length)); + } + _ => unreachable!("Expected OpVariable or OpFunctionParameter"), + }; + + new_vfp_map.insert(vfp_idx, (base_id, ta_idx)); + } + + // 5. Change affected OpTypeFunction + for &tf_idx in op_type_function_idxs.iter() { + let tf_result_id = spv[tf_idx + 1]; + + let Some(changes) = function_type_changes.get(&tf_result_id) else { + continue; + }; + + let tf_wc = hiword(spv[tf_idx]) as usize; + let num_params = tf_wc - 3; + + let mut new_params: Vec = vec![]; + let mut change_i = 0; + for i in 0..num_params { + if change_i < changes.len() && changes[change_i].0 == i { + let (_, type_ptr, length) = changes[change_i]; + for _ in 0..length { + new_params.push(type_ptr); + } + change_i += 1; + } else { + new_params.push(spv[tf_idx + 3 + i]); + } } - // Ordering issues with this, let's keep it after all other type pointers. - // - // instruction_inserts.push(InstructionInsert { - // previous_spv_idx: v_idx, - // instruction: new_instruction, - // }); - types_header_insert.instruction.append(&mut new_instruction); - new_variables_map.insert(vfp_idx, base_id); - - let old_result_id = spv[vfp_idx + 2]; - let new_ids = (base_id..base_id + length).collect::>(); - affected_decorations.push(AffectedDecoration { - original_res_id: old_result_id, - new_res_ids: new_ids, - correction_type: CorrectionType::SplitBindingArray(length), - }); + + new_spv[tf_idx..tf_idx + tf_wc].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + + let new_wc = (3 + new_params.len()) as u16; + let mut new_tf = vec![ + encode_word(new_wc, SPV_INSTRUCTION_OP_TYPE_FUNCTION), + tf_result_id, + spv[tf_idx + 2], // return type (unchanged) + ]; + new_tf.extend_from_slice(&new_params); + types_header_insert.instruction.extend_from_slice(&new_tf); } let access_idxs = op_access_chain_idxs @@ -260,7 +330,7 @@ pub fn splitbindingarray( }) .collect::>(); - // 5. Trace array samplers into a map + // 6. Trace array samplers into a map // Arrayed samplers turn our neat trace tree into a DAG. // To keep things simple, we handle samplers separately. // See `opaque_trace.rs` for details. @@ -284,7 +354,7 @@ pub fn splitbindingarray( } } - // 6. Replace OpAccessChain with selection function + // 7. Replace OpAccessChain with selection function for &(ac_idx, vfp_idx, ta_idx, array_type) in access_idxs.iter() { let ac_word_count = hiword(spv[ac_idx]) as usize; new_spv[ac_idx..ac_idx + ac_word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); @@ -294,7 +364,7 @@ pub fn splitbindingarray( let length = length_map[&ta_idx]; - let base_id = new_variables_map[vfp_idx]; + let (base_id, _) = new_vfp_map[vfp_idx]; if let Some(OpaqueArrayType) = *array_type { // When both a texture array and a sampler array feed the same OpSampledImage, @@ -365,7 +435,7 @@ pub fn splitbindingarray( if let Some((sampler_array_ac_idx, sampler_array_v_idx, sampler_array_ta_idx)) = maybe_sampler_array_data { - let sampler_base_id = new_variables_map[sampler_array_v_idx]; + let (sampler_base_id, _) = new_vfp_map[sampler_array_v_idx]; let sampler_index_0_id = spv[sampler_array_ac_idx + 4]; let sampler_length = length_map[sampler_array_ta_idx] as usize; @@ -578,12 +648,58 @@ pub fn splitbindingarray( } } - // 7. Find OpDecorate / OpName to OpVariable + // 8. Replace all OpFunctionCall references of arrayed resources + let new_vfp_id_map = new_vfp_map + .iter() + .map(|(&vfp_idx, &v)| { + let result_id = spv[vfp_idx + 2]; + (result_id, v) + }) + .collect::>(); + for &function_call_idx in op_function_call_idxs.iter() { + const ARGUMENT_OFFSET: usize = 4; + let word_count = hiword(spv[function_call_idx]) as usize; + let mut arguments = vec![]; + for &argument_id in spv + .iter() + .take(function_call_idx + word_count) + .skip(function_call_idx + ARGUMENT_OFFSET) + { + if let Some(&(base_id, ta_idx)) = new_vfp_id_map.get(&argument_id) { + let length = length_map[&ta_idx]; + for i in 0..length { + arguments.push(base_id + i); + } + } else { + arguments.push(argument_id) + } + } + + if arguments.len() != word_count - ARGUMENT_OFFSET { + new_spv[function_call_idx..function_call_idx + word_count] + .fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); + let new_instruction = [ + &[encode_word( + (arguments.len() + ARGUMENT_OFFSET) as u16, + SPV_INSTRUCTION_OP_FUNCTION_CALL, + )], + &spv[function_call_idx + 1..function_call_idx + ARGUMENT_OFFSET], + arguments.as_slice(), + ] + .concat(); + instruction_inserts.push(InstructionInsert { + previous_spv_idx: function_call_idx, + instruction: new_instruction, + }); + } + } + + // 9. Find OpDecorate / OpName to OpVariable let unused_decorate_idxs = op_decorate_idxs .iter() .filter(|&idx| { let target = spv[idx + 1]; - new_variables_map.iter().any(|(vfp_idx, _)| { + new_vfp_map.iter().any(|(vfp_idx, _)| { let result_id = spv[vfp_idx + 2]; target == result_id }) @@ -594,7 +710,7 @@ pub fn splitbindingarray( .iter() .filter(|&idx| { let target = spv[idx + 1]; - new_variables_map.iter().any(|(vfp_idx, _)| { + new_vfp_map.iter().any(|(vfp_idx, _)| { let result_id = spv[vfp_idx + 2]; target == result_id }) @@ -602,7 +718,7 @@ pub fn splitbindingarray( .copied() .collect::>(); - // 8. Remove Instructions that have been Whited Out. + // 10. Remove Instructions that have been Whited Out. for &spv_idx in unused_decorate_idxs.iter().chain(unused_name_idxs.iter()) { let op = spv[spv_idx]; let word_count = hiword(op) as usize; @@ -610,7 +726,7 @@ pub fn splitbindingarray( new_spv[spv_idx..spv_idx + word_count].fill(encode_word(1, SPV_INSTRUCTION_OP_NOP)); } - // 9. OpDecorate + // 11. OpDecorate let DecorateOut { descriptor_sets_to_correct, } = util::decorate(DecorateIn { @@ -622,17 +738,17 @@ pub fn splitbindingarray( corrections, }); - // 10. Insert New Instructions + // 12. Insert New Instructions instruction_inserts.insert(0, types_header_insert); insert_new_instructions(&spv, &mut new_spv, &word_inserts, &instruction_inserts); - // 11. Correct OpDecorate Bindings + // 13. Correct OpDecorate Bindings util::correct_decorate(CorrectDecorateIn { new_spv: &mut new_spv, descriptor_sets_to_correct, }); prune_noops(&mut new_spv); - // 12. Write New Header and New Code + // 14. Write New Header and New Code Ok(fuse_final(spv_header, new_spv, instruction_bound)) } From 6a410190de04858eb86076a24a2d494c4b269440 Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 27 May 2026 14:05:28 -0700 Subject: [PATCH 12/16] Add texture_array_binding_array test --- README.md | 50 +++++++++++++++++- src/test.rs | 18 ++++--- src/test/splitbindingarray/compile.sh | 1 + .../texture_array_binding_array.frag | 15 ++++++ .../texture_array_binding_array.spv | Bin 0 -> 1204 bytes 5 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 src/test/splitbindingarray/texture_array_binding_array.frag create mode 100644 src/test/splitbindingarray/texture_array_binding_array.spv diff --git a/README.md b/README.md index 55eb7ec..284a50e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ At the moment, the following transformations are supported: | --------------------------------- | ----------- | ------ | ------ | | Combined Image Samplers | ✅ | ✅ | ✅ | | Immediates (Push Constants) | ✅ | ✅\* | ✅ | +| Binding Arrays | ✅ | ✅ | ✅ | | Mixed Depth / Comparison | ✅ | ⚠️\* | ❌ | | isnan / isinf Patching | ✅ | ✅ | ✅ | | Storage Cube Patching | ✅ | ✅ | ✅ | @@ -89,7 +90,6 @@ layout(std140, set = N+1, binding = 0) uniform PushBlock { } pc; // where N is the max set in the shader. - ``` ### Additional Notes @@ -110,6 +110,54 @@ layout(std140, set = N+1, binding = 0) uniform PushBlock { > \* naga's SPIR-V front-end rejects `MatrixStride 16` for `mat2x2`, this should be fixed soon (?). +## Binding Arrays + +Binding arrays are a feature commonly used in shaders and supported by WGSL compilers, just not on the web (yet?). +This patch takes fixed size arrays of size N containing opaque or concrete types and splits them into N individual bindings. +In `wgpu`, this covers following features: + +- `TEXTURE_BINDING_ARRAY` +- `BUFFER_BINDING_ARRAY` +- `STORAGE_RESOURCE_BINDING_ARRAY` +- `SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING` +- `STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING` + +```glsl +#define MAX_RESOURCES 3 +layout(set = 0, binding = 0) uniform sampler u_samplers[MAX_RESOURCES]; +layout(set = 0, binding = 1) uniform texture2D u_textures[MAX_RESOURCES]; +layout(set = 0, binding = 2) uniform Thing { float a; } u_things[MAX_RESOURCES]; + +// is converted into... + +layout(set = 0, binding = 0) uniform sampler u_samplers_0; +layout(set = 0, binding = 1) uniform sampler u_samplers_1; +layout(set = 0, binding = 2) uniform sampler u_samplers_2; +layout(set = 0, binding = 3) uniform texture2D u_textures_0; +layout(set = 0, binding = 4) uniform texture2D u_textures_1; +layout(set = 0, binding = 5) uniform texture2D u_textures_2; +layout(set = 0, binding = 6) uniform Thing { float a; } u_things_0; +layout(set = 0, binding = 7) uniform Thing { float a; } u_things_1; +layout(set = 0, binding = 8) uniform Thing { float a; } u_things_2; +``` + +### Additional Notes + +- Combined image samplers are not supported, please run the combined image sampler pass first. +- Nested resources (`texture2D u[I][J][K]`) are not supported. +- Usage of additional SPIR-V capabilities such as `SparseResidency` or `ImageQuery` are not supported. + +### Tests + +| Test | `spirv-val` | Naga | Tint | +| ------------------------------------- | ----------- | ------ | ---- | +| `buffer_binding_array.frag` | ✅ | ✅ | ✅ | +| `storage_binding_array.frag` | ✅ | ✅ | ✅ | +| `texture_binding_array.frag` | ✅ | ✅ | ✅ | +| `nested_texture_binding_array.frag` | ✅ | ✅ | ✅ | +| `sampler_binding_array.frag` | ✅ | ✅ | ✅ | +| `sampler_stub.frag` | ✅ | ✅ | ✅ | + ## Mixed Depth / Comparison The WGSL spec differentiates between `sampler` and `sampler_comparison` as well as `texture2d` and `texture_depth_2d`. diff --git a/src/test.rs b/src/test.rs index 6348867..c0286e8 100644 --- a/src/test.rs +++ b/src/test.rs @@ -259,38 +259,44 @@ test_with_spv_and_fn_no_correction![ // --- test_with_spv_and_fn![ - splitbinding_splitbinding_buffer_binding_array, + splitbinding_buffer_binding_array, DO_ALL, "./test/splitbindingarray/buffer_binding_array.spv", splitbindingarray ]; test_with_spv_and_fn![ - splitbinding_splitbinding_storage_binding_array, + splitbinding_storage_binding_array, DO_ALL, "./test/splitbindingarray/storage_binding_array.spv", splitbindingarray ]; test_with_spv_and_fn![ - splitbinding_splitbinding_texture_binding_array, + splitbinding_texture_binding_array, DO_ALL, "./test/splitbindingarray/texture_binding_array.spv", splitbindingarray ]; test_with_spv_and_fn![ - splitbinding_splitbinding_nested_texture_binding_array, + splitbinding_nested_texture_binding_array, DO_ALL, "./test/splitbindingarray/nested_texture_binding_array.spv", splitbindingarray ]; test_with_spv_and_fn![ - splitbinding_splitbinding_sampler_binding_array, + splitbinding_sampler_binding_array, DO_ALL, "./test/splitbindingarray/sampler_binding_array.spv", splitbindingarray ]; test_with_spv_and_fn![ - splitbinding_splitbinding_sampler_stub, + splitbinding_sampler_stub, DO_ALL, "./test/splitbindingarray/sampler_stub.spv", splitbindingarray ]; +test_with_spv_and_fn![ + splitbinding_texture_array_binding_array, + DO_ALL, + "./test/splitbindingarray/texture_array_binding_array.spv", + splitbindingarray +]; diff --git a/src/test/splitbindingarray/compile.sh b/src/test/splitbindingarray/compile.sh index ad854a1..8106c31 100755 --- a/src/test/splitbindingarray/compile.sh +++ b/src/test/splitbindingarray/compile.sh @@ -6,3 +6,4 @@ glslc -O0 texture_binding_array.frag -o texture_binding_array.spv glslc -O0 sampler_binding_array.frag -o sampler_binding_array.spv glslc -O0 nested_texture_binding_array.frag -o nested_texture_binding_array.spv glslc -O0 sampler_stub.frag -o sampler_stub.spv +glslc -O0 texture_array_binding_array.frag -o texture_array_binding_array.spv diff --git a/src/test/splitbindingarray/texture_array_binding_array.frag b/src/test/splitbindingarray/texture_array_binding_array.frag new file mode 100644 index 0000000..054a0fb --- /dev/null +++ b/src/test/splitbindingarray/texture_array_binding_array.frag @@ -0,0 +1,15 @@ +#version 440 + +#define MAX_TEXTURES 8 + +layout(location = 0) out vec4 o_color; + +layout(set = 0, binding = 0) uniform texture2DArray u_textures[MAX_TEXTURES]; +layout(set = 0, binding = 1) uniform sampler u_sampler; + +void main() { + o_color = vec4(0.0); + for (int i = 0; i < MAX_TEXTURES; i++) { + o_color += texture(sampler2DArray(u_textures[i], u_sampler), vec3(0.0, 0.0, 0.0)); + } +} diff --git a/src/test/splitbindingarray/texture_array_binding_array.spv b/src/test/splitbindingarray/texture_array_binding_array.spv new file mode 100644 index 0000000000000000000000000000000000000000..1eef625e96c587612ca10b145b677b2449199e2a GIT binary patch literal 1204 zcmZ9MOKTKS5QT3tnHUrEGB5K`draahZX_9BX zH0fr;v@^~=rt%glysX#BC*3Nc4x@Uex##wiPCxGtL!C2M$TPgo-!0-K8K)WWmYjO4b1%Hibo$&{C*NGdr=KdzpedBBPeTyyJ9^W`k?Qcvm-{dzO^L5-_*19fh zZ?m@cEwvG`fvcyu95wN~6gOADySUn&QKN46aXI22z?|^}7gsmy#G1Hsf1Pq3O)l-nGTryMCeGx&2WQ^9@QJ#i}|w^B-R5CH@yW#!z?w literal 0 HcmV?d00001 From aaaad6c481d3f2f115d98d906fee5f244ff2feed Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 27 May 2026 14:05:37 -0700 Subject: [PATCH 13/16] Add README.md --- README.md | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 284a50e..1ccffab 100644 --- a/README.md +++ b/README.md @@ -123,22 +123,36 @@ In `wgpu`, this covers following features: - `STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING` ```glsl -#define MAX_RESOURCES 3 +struct Thing { + float a; +}; + +#define MAX_RESOURCES 2 layout(set = 0, binding = 0) uniform sampler u_samplers[MAX_RESOURCES]; layout(set = 0, binding = 1) uniform texture2D u_textures[MAX_RESOURCES]; -layout(set = 0, binding = 2) uniform Thing { float a; } u_things[MAX_RESOURCES]; +layout(set = 0, binding = 2) uniform texture2DArray u_texture_arrays[MAX_RESOURCES]; +layout(set = 0, binding = 3) uniform image2D u_images[MAX_RESOURCES]; +layout(set = 0, binding = 4) uniform Thing u_things[MAX_RESOURCES]; +layout(set = 0, binding = 5, std140) buffer Thing u_buf_things[MAX_RESOURCES]; // is converted into... +struct Thing { + float a; +}; + layout(set = 0, binding = 0) uniform sampler u_samplers_0; layout(set = 0, binding = 1) uniform sampler u_samplers_1; -layout(set = 0, binding = 2) uniform sampler u_samplers_2; -layout(set = 0, binding = 3) uniform texture2D u_textures_0; -layout(set = 0, binding = 4) uniform texture2D u_textures_1; -layout(set = 0, binding = 5) uniform texture2D u_textures_2; -layout(set = 0, binding = 6) uniform Thing { float a; } u_things_0; -layout(set = 0, binding = 7) uniform Thing { float a; } u_things_1; -layout(set = 0, binding = 8) uniform Thing { float a; } u_things_2; +layout(set = 0, binding = 2) uniform texture2D u_textures_0; +layout(set = 0, binding = 3) uniform texture2D u_textures_1; +layout(set = 0, binding = 4) uniform texture2DArray u_texture_arrays_0; +layout(set = 0, binding = 5) uniform texture2DArray u_texture_arrays_1; +layout(set = 0, binding = 6) uniform image2D u_images_0; +layout(set = 0, binding = 7) uniform image2D u_images_1; +layout(set = 0, binding = 8) uniform Thing u_things_0; +layout(set = 0, binding = 9) uniform Thing u_things_1; +layout(set = 0, binding = 10, std140) buffer Thing u_buf_things_0; +layout(set = 0, binding = 11, std140) buffer Thing u_buf_things_1; ``` ### Additional Notes @@ -157,6 +171,7 @@ layout(set = 0, binding = 8) uniform Thing { float a; } u_things_2; | `nested_texture_binding_array.frag` | ✅ | ✅ | ✅ | | `sampler_binding_array.frag` | ✅ | ✅ | ✅ | | `sampler_stub.frag` | ✅ | ✅ | ✅ | +| `texture_array_binding_array.frag` | ✅ | ✅ | ✅ | ## Mixed Depth / Comparison From ef44d72d07ff6e22ce757bec1446163f825d3d71 Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 27 May 2026 14:12:56 -0700 Subject: [PATCH 14/16] Update docs --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 47d5eb0..59c1da4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ //! | --------------------------------- | ----------- | ------ | ------ | //! | Combined Image Samplers | ✅ | ✅ | ✅ | //! | Immediates (Push Constants) | ✅ | ✅\* | ✅ | +//! | Binding Arrays | ✅ | ✅ | ✅ | //! | Mixed Depth / Comparison | ✅ | ⚠️\* | ❌ | //! | isnan / isinf Patching | ✅ | ✅ | ✅ | //! | Storage Cube Patching | ✅ | ✅ | ✅ | From 5c0f0c16d87862c3de1e3d0cf5a6081fb623a6fb Mon Sep 17 00:00:00 2001 From: davnotdev Date: Wed, 27 May 2026 15:06:10 -0700 Subject: [PATCH 15/16] Update ffi for splitbindingarray --- ffi/bin/README.txt | 2 ++ ffi/bin/spv_webgpu_transform.c | 13 +++++++++++++ ffi/spirv_webgpu_transform.h | 3 +++ ffi/src/lib.rs | 33 ++++++++++++++++++++++++++++++++- src/correction.rs | 7 ++----- src/splitbindingarray.rs | 2 +- src/util/opaque_trace.rs | 4 +++- 7 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 ffi/bin/README.txt diff --git a/ffi/bin/README.txt b/ffi/bin/README.txt new file mode 100644 index 0000000..fe9e20d --- /dev/null +++ b/ffi/bin/README.txt @@ -0,0 +1,2 @@ +Make sure to update this guy when a new patch is included in the API. +The main purpose of this file is not to test functionality but linking! diff --git a/ffi/bin/spv_webgpu_transform.c b/ffi/bin/spv_webgpu_transform.c index 148563c..5a3e807 100644 --- a/ffi/bin/spv_webgpu_transform.c +++ b/ffi/bin/spv_webgpu_transform.c @@ -40,6 +40,14 @@ int main() { uint32_t pruneunuseddref_out_count; spirv_webgpu_transform_pruneunuseddref_alloc(storagecube_out_spv, storagecube_out_count, &pruneunuseddref_out_spv, &pruneunuseddref_out_count); + uint32_t *immediates_out_spv; + uint32_t immediates_out_count; + spirv_webgpu_transform_immediatespatch_alloc(pruneunuseddref_out_spv, pruneunuseddref_out_count, &immediates_out_spv, &immediates_out_count); + + uint32_t *splitbindingarray_out_spv; + uint32_t splitbindingarray_out_count; + spirv_webgpu_transform_splitbindingarray_alloc(immediates_out_spv, immediates_out_count, &splitbindingarray_out_spv, &splitbindingarray_out_count, &correction_map); + // 3. Observe the patched variables print_set_binding(correction_map, 0, 0); print_set_binding(correction_map, 0, 1); @@ -51,6 +59,8 @@ int main() { print_set_binding(correction_map, 3, 0); // 4. Free memory + spirv_webgpu_transform_splitbindingarray_free(splitbindingarray_out_spv); + spirv_webgpu_transform_immediatespatch_free(immediates_out_spv); spirv_webgpu_transform_pruneunuseddref_free(pruneunuseddref_out_spv); spirv_webgpu_transform_storagecubepatch_free(storagecube_out_spv); spirv_webgpu_transform_isnanisinfpatch_free(isnanisinf_out_spv); @@ -88,6 +98,9 @@ void print_set_binding(TransformCorrectionMap map, uint32_t set, uint32_t bindin case SPIRV_WEBGPU_TRANSFORM_CORRECTION_TYPE_CONVERT_STORAGE_CUBE: printf("CONVERT_STORAGE_CUBE "); break; + case SPIRV_WEBGPU_TRANSFORM_CORRECTION_TYPE_SPLIT_BINDING_ARRAY: + printf("SPLIT_BINDING_ARRAY "); + break; } } printf("\n"); diff --git a/ffi/spirv_webgpu_transform.h b/ffi/spirv_webgpu_transform.h index d28041a..3fa6545 100644 --- a/ffi/spirv_webgpu_transform.h +++ b/ffi/spirv_webgpu_transform.h @@ -23,6 +23,8 @@ void spirv_webgpu_transform_storagecubepatch_alloc(uint32_t *in_spv, uint32_t in void spirv_webgpu_transform_storagecubepatch_free(uint32_t *out_spv); void spirv_webgpu_transform_pruneunuseddref_alloc(uint32_t *int_spv, uint32_t in_count, uint32_t **out_spv, uint32_t *out_count); void spirv_webgpu_transform_pruneunuseddref_free(uint32_t *out_spv); +void spirv_webgpu_transform_splitbindingarray_alloc(uint32_t *in_spv, uint32_t in_count, uint32_t **out_spv, uint32_t *out_count, TransformCorrectionMap *correction_map); +void spirv_webgpu_transform_splitbindingarray_free(uint32_t *out_spv); void spirv_webgpu_transform_mirrorpatch_alloc( uint32_t *in_left_spv, uint32_t in_left_count, TransformCorrectionMap *left_corrections, @@ -41,6 +43,7 @@ typedef enum { SPIRV_WEBGPU_TRANSFORM_CORRECTION_TYPE_SPLIT_DREF_REGULAR = 1, SPIRV_WEBGPU_TRANSFORM_CORRECTION_TYPE_SPLIT_DREF_COMPARISON = 2, SPIRV_WEBGPU_TRANSFORM_CORRECTION_TYPE_CONVERT_STORAGE_CUBE = 3, + SPIRV_WEBGPU_TRANSFORM_CORRECTION_TYPE_SPLIT_BINDING_ARRAY = 4, } TransformCorrectionType; // SAFETY: `corrections` invalidates when `correction_map` is written to. diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 8de424d..ff57a83 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -3,7 +3,7 @@ use core::{ffi, ptr, slice}; use spirv_webgpu_transform::{ CorrectionMap, combimgsampsplitter, drefsplitter, immediatespatch, isnanisinfpatch, - mirrorpatch, pruneunuseddref, storagecubepatch, + mirrorpatch, pruneunuseddref, splitbindingarray, storagecubepatch, }; type TransformCorrectionMap = *mut ffi::c_void; @@ -199,6 +199,35 @@ pub unsafe extern "C" fn spirv_webgpu_transform_pruneunuseddref_free(out_spv: *m unsafe { drop(Box::from_raw(out_spv)) } } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn spirv_webgpu_transform_splitbindingarray_alloc( + in_spv: *const u32, + in_count: u32, + out_spv: *mut *const u32, + out_count: *mut u32, + correction_map: *mut TransformCorrectionMap, +) { + let correction_map = unsafe { alloc_or_pass_correction_map(correction_map) }; + + let in_spv = unsafe { slice::from_raw_parts(in_spv, in_count as usize) }; + match splitbindingarray(in_spv, correction_map) { + Ok(spv) => unsafe { + *out_count = spv.len() as u32; + let leaked = Box::leak(spv.into_boxed_slice()); + *out_spv = leaked.as_ptr(); + }, + Err(_) => unsafe { + *out_spv = ptr::null(); + *out_count = 0; + }, + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn spirv_webgpu_transform_splitbindingarray_free(out_spv: *mut u32) { + unsafe { drop(Box::from_raw(out_spv)) } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn spirv_webgpu_transform_mirrorpatch_alloc( in_left_spv: *const u32, @@ -267,6 +296,8 @@ pub enum TransformCorrectionType { SpirvWebgpuTransformCorrectionTypeSplitCombined = 0, SpirvWebgpuTransformCorrectionTypeSplitDrefRegular = 1, SpirvWebgpuTransformCorrectionTypeSplitDrefComparison = 2, + SpirvWebgpuTransformCorrectionTypeConvertStorageCube = 3, + SpirvWebgpuTransformCorrectionTypeSplitBindingArray = 4, } // TransformCorrectionStatus spirv_webgpu_transform_correction_map_index(uint32_t set, uint32_t binding, TransformCorrectionType** corrections_ptr, uint32_t* correction_count); diff --git a/src/correction.rs b/src/correction.rs index 6dc91c2..2657599 100644 --- a/src/correction.rs +++ b/src/correction.rs @@ -14,11 +14,8 @@ pub enum CorrectionType { SplitDrefComparison, /// A storage cube texture has been converted into a storage texture 2D array, change the dimension. ConvertStorageCube, - /// A binding array has been split into N new variables. - /// In ffi form, this is represented by - /// `SPLIT_BINDING_ARRAY_BASE <= v < SPLIT_BINDING_ARRAY_MAX` - /// where N = `v - SPLIT_BINDING_ARRAY_BASE`. - SplitBindingArray(u32), + /// A binding array has been split into new variables. Insert the same resource again. + SplitBindingArray, } #[derive(Debug, Clone, Default, PartialEq, Eq)] diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index 350c0fd..d56a22c 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -245,7 +245,7 @@ pub fn splitbindingarray( affected_decorations.push(AffectedDecoration { original_res_id: old_result_id, new_res_ids: new_ids, - correction_type: CorrectionType::SplitBindingArray(length), + correction_type: CorrectionType::SplitBindingArray, }); } SPV_INSTRUCTION_OP_FUNCTION_PARAMETER => { diff --git a/src/util/opaque_trace.rs b/src/util/opaque_trace.rs index 7eff894..7bd6041 100644 --- a/src/util/opaque_trace.rs +++ b/src/util/opaque_trace.rs @@ -387,7 +387,9 @@ pub fn reconstruct_opaque_trace_and_overwrite( out.extend_from_slice(take_instruction(spv, op_idx)); write_nop_instruction(new_spv, op_idx); } - OpaqueImageOp::Sampled(SampledImageOp { idx: si_idx, next, .. }) => { + OpaqueImageOp::Sampled(SampledImageOp { + idx: si_idx, next, .. + }) => { out.extend_from_slice(take_instruction(spv, *si_idx)); write_nop_instruction(new_spv, *si_idx); From e82518d2bb393f1420e264a4dfbfa87c9bddfe2a Mon Sep 17 00:00:00 2001 From: davnotdev Date: Thu, 28 May 2026 09:06:46 -0700 Subject: [PATCH 16/16] N-1 binding array corrections --- src/correction.rs | 1 + src/splitbindingarray.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/correction.rs b/src/correction.rs index 2657599..ec3175c 100644 --- a/src/correction.rs +++ b/src/correction.rs @@ -15,6 +15,7 @@ pub enum CorrectionType { /// A storage cube texture has been converted into a storage texture 2D array, change the dimension. ConvertStorageCube, /// A binding array has been split into new variables. Insert the same resource again. + /// For an `N` sized array, expect `N-1` entries. SplitBindingArray, } diff --git a/src/splitbindingarray.rs b/src/splitbindingarray.rs index d56a22c..96f9c9f 100644 --- a/src/splitbindingarray.rs +++ b/src/splitbindingarray.rs @@ -241,7 +241,22 @@ pub fn splitbindingarray( .instruction .append(&mut new_type_instructions); let old_result_id = spv[vfp_idx + 2]; - let new_ids = (base_id..base_id + length).collect::>(); + + // We manually correct the base variable to reuse the original decorations. + // That way, we can output `N-1` correction bindings. + for &d_idx in op_decorate_idxs.iter() { + if spv[d_idx + 1] == old_result_id { + new_spv[d_idx + 1] = base_id; + } + } + for &n_idx in op_name_idxs.iter() { + if spv[n_idx + 1] == old_result_id { + new_spv[n_idx + 1] = base_id; + } + } + + // We only want `N-1` correction bindings. + let new_ids = (base_id + 1..base_id + length).collect::>(); affected_decorations.push(AffectedDecoration { original_res_id: old_result_id, new_res_ids: new_ids, @@ -697,8 +712,11 @@ pub fn splitbindingarray( // 9. Find OpDecorate / OpName to OpVariable let unused_decorate_idxs = op_decorate_idxs .iter() - .filter(|&idx| { + .filter(|&&idx| { let target = spv[idx + 1]; + if new_spv[idx + 1] != target { + return false; + } new_vfp_map.iter().any(|(vfp_idx, _)| { let result_id = spv[vfp_idx + 2]; target == result_id @@ -708,8 +726,11 @@ pub fn splitbindingarray( .collect::>(); let unused_name_idxs = op_name_idxs .iter() - .filter(|&idx| { + .filter(|&&idx| { let target = spv[idx + 1]; + if new_spv[idx + 1] != target { + return false; + } new_vfp_map.iter().any(|(vfp_idx, _)| { let result_id = spv[vfp_idx + 2]; target == result_id