fix(renderer): cap mesh writes at MESH_MAX_VERTS

This commit is contained in:
L'électron rare
2026-05-13 12:23:38 +02:00
parent d033282aa3
commit 498ca2be70
2 changed files with 58 additions and 4 deletions
+6 -3
View File
@@ -243,7 +243,9 @@ class MetalRenderer(NSObject):
def _init_mesh_cpu_buffer(self) -> None:
if getattr(self, "_mesh_cpu_buf", None) is None:
self._mesh_cpu_buf = np.zeros(MESH_MAX_VERTS * 5, dtype=np.float32)
self._mesh_cpu_buf = np.zeros(
MESH_MAX_VERTS * MESH_VERT_FLOATS, dtype=np.float32,
)
# ---- Uniforms helpers ------------------------------------------
def _update_uniforms(self) -> int:
@@ -401,7 +403,7 @@ class MetalRenderer(NSObject):
ou triangle invalide (confiance basse)."""
nonlocal n_verts
tris = n_verts // 3
if tris >= MESH_MAX_TRIS:
if tris >= MESH_MAX_TRIS or n_verts + 3 > MESH_MAX_VERTS:
return False
if i >= len(kp_list) or j >= len(kp_list) or k >= len(kp_list):
return True # skip mais continue
@@ -464,7 +466,8 @@ class MetalRenderer(NSObject):
if n_verts == 0:
return 0
data = self._mesh_cpu_buf[: n_verts * 5].tobytes()
# Slice is exact — stale floats beyond n_verts*MESH_VERT_FLOATS never reach the GPU.
data = self._mesh_cpu_buf[: n_verts * MESH_VERT_FLOATS].tobytes()
mv = self._mesh_buf.contents().as_buffer(len(data))
mv[:] = data
return n_verts // 3
@@ -1,10 +1,15 @@
# data_only_viz/tests/test_renderer_allocations.py
"""Renderer must reuse a preallocated CPU staging buffer for the skeleton."""
import time
import types
import unittest.mock as mock
import numpy as np
import pytest
from data_only_viz.renderer import MetalRenderer, SKEL_MAX_SEGS, MESH_MAX_VERTS
from data_only_viz.state import State
from data_only_viz.state import PoseKp, State
def test_skeleton_cpu_buffer_is_preallocated_and_reused():
@@ -38,3 +43,49 @@ def test_mesh_cpu_buffer_is_preallocated_and_reused():
assert buf.size == MESH_MAX_VERTS * 5
r._init_mesh_cpu_buffer()
assert r._mesh_cpu_buf is buf
def test_mesh_buffer_overflow_guard():
"""push_tri must stop writing before exceeding MESH_MAX_VERTS.
Strategy: build a State with enough face-person entries that the old
MESH_MAX_TRIS-only guard would allow n_verts > MESH_MAX_VERTS (old cap
was MESH_MAX_TRIS*3 = 24576), whereas the new guard caps at MESH_MAX_VERTS
(10475). FACE_TRIANGLES has 36 tris → 108 verts per person.
200 persons × 108 = 21600 verts > 10475, so the vertex guard fires first.
_update_mesh calls self._mesh_buf.contents().as_buffer() — we stub that
out since no GPU is available in unit tests.
"""
from data_only_viz.renderer import MESH_MAX_VERTS
r = MetalRenderer.__new__(MetalRenderer)
r._init_mesh_cpu_buffer()
# Stub _mesh_buf so the GPU upload path does not crash.
fake_mv = bytearray(MESH_MAX_VERTS * 5 * 4) # max possible bytes
class FakeBuf:
def contents(self):
return self
def as_buffer(self, n):
return memoryview(fake_mv)[:n]
r._mesh_buf = FakeBuf()
# Build a State with 200 face persons, each with 478 keypoints at conf=1.0.
# With FACE_TRIANGLES (36 tris = 108 verts/person), 200 persons would write
# 21600 verts — well past MESH_MAX_VERTS — if the guard were absent.
s = State()
s.pose_last_t = time.monotonic() # make pose_alive() return True
visible_kp = [PoseKp(x=0.5, y=0.5, z=0.0, c=1.0) for _ in range(478)]
s.persons_face = [list(visible_kp) for _ in range(200)]
n_tris = r._update_mesh(s)
n_verts = n_tris * 3
assert n_verts <= MESH_MAX_VERTS, (
f"Buffer overflow: _update_mesh wrote {n_verts} verts, "
f"but MESH_MAX_VERTS={MESH_MAX_VERTS}"
)