snap: fix possible crash

- use draw handler to move the expensive synchronous GPU readback
out of the mouse-event loop.
- restore GPU state (depth, blend, face culling) around the offscreen
This commit is contained in:
Bruno Perdigão
2026-08-31 18:56:19 -03:00
committed by Bruno Perdigão
parent 0f629d0d54
commit a6d59bf61c
4 changed files with 122 additions and 26 deletions
@@ -2874,3 +2874,54 @@ class WallSystemPathDecorator(_ConnectedNetworkPathDecorator):
else:
result.append(point)
return result
class GpuSnapDecorator(tool.Blender.ViewportDecorator):
"""Draw-handler that performs GPU snap detection once per viewport draw.
The detection work is injected via ``install(..., detection=...)`` so this
decorator has no knowledge of the raycast or snap subsystems. The modal
snapping code sets the current request via :meth:`set_request` and reads the
decoded result from :meth:`get_cache`.
"""
draw_method = "draw"
request: dict[str, Any] = {"mouse_x": 0, "mouse_y": 0, "objs_to_raycast": []}
cache: tuple[list[dict], bpy.types.Object | None, list[dict]] | None = None
detection: Any = None
event: Any = None
@classmethod
def install(cls, context: bpy.types.Context, event=None, detection=None) -> None:
if detection is not None:
cls.detection = detection
cls.event = event
cls.request = {"mouse_x": 0, "mouse_y": 0, "objs_to_raycast": []}
cls.cache = None
super().install(context)
@classmethod
def uninstall(cls) -> None:
cls.cache = None
cls.event = None
super().uninstall()
@classmethod
def set_request(cls, event, objs_to_raycast: list[bpy.types.Object]) -> None:
cls.event = event
cls.request["mouse_x"] = int(event.mouse_region_x)
cls.request["mouse_y"] = int(event.mouse_region_y)
cls.request["objs_to_raycast"] = objs_to_raycast
@classmethod
def get_cache(cls) -> tuple[list[dict], bpy.types.Object | None, list[dict]] | None:
return cls.cache
def draw(self, context: bpy.types.Context) -> None:
cls = type(self)
if cls.detection is None or cls.event is None or not cls.request["objs_to_raycast"]:
cls.cache = None
return
cls.cache = cls.detection(context, cls.event, cls.request)
@@ -26,7 +26,7 @@ import ifcopenshell.util.unit
from mathutils import Vector
import bonsai.tool as tool
from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.model.decorator import GpuSnapDecorator, PolylineDecorator
class PolylineOperator:
@@ -424,6 +424,7 @@ class PolylineOperator:
def cleanup(self, context: bpy.Types.Context):
context.workspace.status_text_set(text=None)
PolylineDecorator.uninstall()
GpuSnapDecorator.uninstall()
tool.Polyline.clear_polyline()
tool.Raycast.clear_cache()
tool.Blender.update_viewport()
@@ -460,6 +461,7 @@ class PolylineOperator:
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> None:
PolylineDecorator.install(context)
GpuSnapDecorator.install(context, event, detection=tool.Raycast.detect_gpu_snaps)
tool.Snap.clear_snapping_point()
self.tool_state.use_default_container = False
+43 -19
View File
@@ -1108,6 +1108,13 @@ class Raycast(bonsai.core.tool.Raycast):
if _offscreen is None:
_offscreen = GPUOffScreen(max(w, 1), max(h, 1), format="RGBA8")
# Save GPU state so it can be restored even if readback fails.
prev_depth_mask = gpu.state.depth_mask_get()
prev_depth_test = gpu.state.depth_test_get()
prev_blend = gpu.state.blend_get()
face_culling_get = getattr(gpu.state, "face_culling_get", None)
prev_face_culling = face_culling_get() if face_culling_get is not None else None
_encoding_shader.bind()
if xray_mode:
@@ -1137,27 +1144,30 @@ class Raycast(bonsai.core.tool.Raycast):
buffers_list = []
last_buf = None
with _offscreen.bind():
fb = gpu.state.active_framebuffer_get()
fb.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0)
try:
with _offscreen.bind():
fb = gpu.state.active_framebuffer_get()
fb.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0)
for batch, world_mat, slot_base in render_ops:
mvp = rv3d.perspective_matrix @ world_mat
_encoding_shader.uniform_float("MVP", mvp)
_encoding_shader.uniform_float("slot_base", float(slot_base))
with gpu.matrix.push_pop():
gpu.matrix.load_matrix(Matrix.Identity(4))
batch.draw(_encoding_shader)
for batch, world_mat, slot_base in render_ops:
mvp = rv3d.perspective_matrix @ world_mat
_encoding_shader.uniform_float("MVP", mvp)
_encoding_shader.uniform_float("slot_base", float(slot_base))
with gpu.matrix.push_pop():
gpu.matrix.load_matrix(Matrix.Identity(4))
batch.draw(_encoding_shader)
if read_per_object: # gets all buffers
buf = fb.read_color(int(read_x), int(read_y), read_size, read_size, 4, 0, "UBYTE")
buffers_list.append(buf)
if not read_per_object:
last_buf = fb.read_color(int(read_x), int(read_y), read_size, read_size, 4, 0, "UBYTE")
# Restore state
gpu.state.depth_mask_set(True)
gpu.state.depth_test_set("LESS")
if read_per_object: # gets all buffers
buf = fb.read_color(int(read_x), int(read_y), read_size, read_size, 4, 0, "UBYTE")
buffers_list.append(buf)
if not read_per_object:
last_buf = fb.read_color(int(read_x), int(read_y), read_size, read_size, 4, 0, "UBYTE")
finally:
gpu.state.depth_mask_set(prev_depth_mask)
gpu.state.depth_test_set(prev_depth_test)
gpu.state.blend_set(prev_blend)
if prev_face_culling is not None:
gpu.state.face_culling_set(prev_face_culling)
mouse_read_rect = (w, h, mx, my, read_x, read_y)
if tris:
@@ -1173,6 +1183,20 @@ class Raycast(bonsai.core.tool.Raycast):
def get_gpu_wireframe_snaps(cls, context, event, objs_to_raycast):
return cls.get_gpu_detection_snaps(context, event, objs_to_raycast)
@classmethod
def detect_gpu_snaps(cls, context, event, request):
"""GPU snap detection callback for ``GpuSnapDecorator``."""
if bpy.app.background:
return None
objs_to_raycast = request["objs_to_raycast"]
if not objs_to_raycast:
return None
solid_snaps, closest_obj = cls.get_gpu_solid_snaps(context, event, objs_to_raycast)
wireframe_snaps, _ = cls.get_gpu_wireframe_snaps(context, event, objs_to_raycast)
return solid_snaps, closest_obj, wireframe_snaps
@classmethod
def clear_cache(cls):
global _wireframe_batch_cache, _wireframe_vert_fmt, _triangle_batch_cache, _triangle_vert_fmt, _encoding_shader, _offscreen, _obj_list
+25 -6
View File
@@ -30,21 +30,21 @@ import bonsai.core.tool
import bonsai.tool as tool
from bonsai.bim.module.drawing.data import DecoratorData
from bonsai.bim.module.drawing.decoration import CutDecorator
from bonsai.bim.module.model.decorator import PolylineDecorator
from bonsai.bim.module.model.decorator import GpuSnapDecorator, PolylineDecorator
if TYPE_CHECKING:
from bonsai.bim.prop import BIMSnapGroups, BIMSnapProperties
def _get_gpu_object_snaps(
def _process_gpu_object_snaps(
context: bpy.types.Context,
event: bpy.types.Event,
objs_to_raycast: list[bpy.types.Object],
snap_faces: list[dict[str, Any]],
closest_obj: bpy.types.Object | None,
wireframe_snaps: list[dict[str, Any]],
xray_mode: bool,
) -> list[dict[str, Any]]:
detected_snaps: list[dict[str, Any]] = []
snap_faces, closest_obj = tool.Raycast.get_gpu_solid_snaps(context, event, objs_to_raycast)
wireframe_snaps, _ = tool.Raycast.get_gpu_wireframe_snaps(context, event, objs_to_raycast)
if not xray_mode:
for snap in snap_faces:
@@ -85,6 +85,17 @@ def _get_gpu_object_snaps(
return detected_snaps
def _get_gpu_object_snaps(
context: bpy.types.Context,
event: bpy.types.Event,
objs_to_raycast: list[bpy.types.Object],
xray_mode: bool,
) -> list[dict[str, Any]]:
snap_faces, closest_obj = tool.Raycast.get_gpu_solid_snaps(context, event, objs_to_raycast)
wireframe_snaps, _ = tool.Raycast.get_gpu_wireframe_snaps(context, event, objs_to_raycast)
return _process_gpu_object_snaps(context, event, snap_faces, closest_obj, wireframe_snaps, xray_mode)
def _get_cpu_object_snaps(
context: bpy.types.Context,
event: bpy.types.Event,
@@ -551,7 +562,15 @@ class Snap(bonsai.core.tool.Snap):
props = cls.get_snap_props()
if props.use_gpu_snapping:
detected_snaps.extend(_get_gpu_object_snaps(context, event, objs_to_raycast, xray_mode))
GpuSnapDecorator.set_request(event, objs_to_raycast)
cached = GpuSnapDecorator.get_cache()
if cached is None:
detected_snaps.extend(_get_gpu_object_snaps(context, event, objs_to_raycast, xray_mode))
else:
snap_faces, closest_obj, wireframe_snaps = cached
detected_snaps.extend(
_process_gpu_object_snaps(context, event, snap_faces, closest_obj, wireframe_snaps, xray_mode)
)
else:
detected_snaps.extend(_get_cpu_object_snaps(context, event, objs_to_raycast, xray_mode))
cut_snaps = _get_cut_object_snaps(context, event)