@@ -67,6 +67,7 @@ def __init__(
6767 self .cut_trigs = None
6868 self .trig_counter = None
6969 self ._js_compute = False
70+ self ._last_clipping_bytes = None
7071
7172 self .n_tets = None
7273 if component is None :
@@ -126,7 +127,11 @@ def update(self, options: RenderOptions):
126127 )
127128 self .gpu_objects .complex_settings .update (options )
128129 if not self ._js_compute :
129- self .build_clip_plane ()
130+ # Only recompute clipping triangles when the plane actually changed
131+ clipping_bytes = bytes (self ._clipping .uniforms )
132+ if clipping_bytes != self ._last_clipping_bytes :
133+ self ._last_clipping_bytes = clipping_bytes
134+ self .build_clip_plane ()
130135 if self .symmetry :
131136 self .n_instances = self ._original_n_instances * self .symmetry .n_copies
132137 self .shader_defines ["SYMMETRY" ] = "1"
@@ -288,6 +293,10 @@ def build_clip_plane(self):
288293 label = "cut_trigs_counter" ,
289294 reuse = self .cut_trigs_counter ,
290295 )
296+ # Explicitly reset counter to 0 — buffer_from_array's reuse optimization
297+ # skips the write when _data matches, but the GPU buffer was modified by
298+ # the previous fill pass's atomicAdd.
299+ write_array_to_buffer (self .trig_counter , np .array ([0 ], dtype = np .uint32 ))
291300 shader_code = read_shader_file (self .compute_shader )
292301 run_compute_shader (
293302 shader_code , self .get_bindings (compute = True , count = True ), 1024 , "build_clip_plane" , defines = self .data .mesh_data .get_shader_defines ()
@@ -335,15 +344,13 @@ def get_export_compute_passes(self, options, buffer_registry):
335344 saved_clipping = self .clipping
336345 self .clipping = self ._clipping
337346
338- # Start with a minimal output buffer — the JS engine will resize
339- # after the first count pass via the count_then_fill mechanism.
340- # Live mode keeps Python's existing (correctly-sized) buffer because
341- # the JS engine binds to the live proxy directly.
342- min_size = 64 # minimum 1 SubTrig element
343- if not buffer_registry .live and self .cut_trigs .size > min_size :
344- self .cut_trigs .destroy ()
347+ # The JS engine owns the output buffer (cut_trigs) and indirect buffer
348+ # entirely via the countThenFill protocol. Python only needs minimal
349+ # placeholders so the buffer registry can assign stable IDs.
350+ min_size = 64
351+ if self .cut_trigs is None or self .cut_trigs .size < min_size :
345352 self .cut_trigs = self .device .createBuffer (
346- size = min_size , usage = BufferUsage .STORAGE , label = "cut_trigs_export "
353+ size = min_size , usage = BufferUsage .STORAGE , label = "cut_trigs "
347354 )
348355
349356 fill_bindings = self .get_bindings (compute = True , count = False )
@@ -355,16 +362,18 @@ def get_export_compute_passes(self, options, buffer_registry):
355362 trig_counter_id = buffer_registry .get_id (self .trig_counter )
356363 cut_trigs_id = buffer_registry .get_id (self .cut_trigs )
357364
358- # Create indirect args buffer
359- indirect_data = np .array ([self .n_vertices , 0 , 0 , 0 ], dtype = np .uint32 )
360- self ._indirect_buffer = buffer_from_array (
361- indirect_data ,
362- usage = BufferUsage .INDIRECT | BufferUsage .STORAGE | BufferUsage .COPY_DST ,
363- label = "clip_indirect" ,
364- )
365+ # Indirect buffer: also JS-owned. Create a minimal placeholder for
366+ # the registry ID; the JS engine creates the real one in initPipelines.
367+ if not hasattr (self , '_indirect_buffer' ) or self ._indirect_buffer is None :
368+ self ._indirect_buffer = self .device .createBuffer (
369+ size = 16 ,
370+ usage = BufferUsage .INDIRECT | BufferUsage .STORAGE | BufferUsage .COPY_DST ,
371+ label = "clip_indirect" ,
372+ )
365373 indirect_buf_id = buffer_registry ._next_id ("indirect" )
366- buffer_registry ._buffers [id (self ._indirect_buffer )] = (indirect_buf_id , self ._indirect_buffer , "indirect" )
367-
374+ buffer_registry ._buffers [id (self ._indirect_buffer )] = (
375+ indirect_buf_id , self ._indirect_buffer , "indirect"
376+ )
368377 self ._export_indirect_buf_id = indirect_buf_id
369378
370379 fill_pass_id = f"clip_fill_{ self ._id } "
0 commit comments