Skip to content

Commit

Permalink
Merge pull request #72 from jonathanhogg/fix_mesh_leak
Browse files Browse the repository at this point in the history
Calculate arrays rather than trimesh objects where possible
  • Loading branch information
jonathanhogg authored Feb 5, 2025
2 parents 25ea2c7 + 66a750c commit 8d509a8
Show file tree
Hide file tree
Showing 7 changed files with 433 additions and 282 deletions.
19 changes: 12 additions & 7 deletions .github/workflows/ci-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,23 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
- name: Upgrade pip
run: |
echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | sudo debconf-set-selections
sudo apt-get install -y mesa-utils xvfb libfontconfig1 ttf-mscorefonts-installer
python -m pip install --upgrade pip setuptools
python -m pip install cython coverage pytest
- name: Build in place with coverage analysis enabled
python -m pip install --upgrade pip
- name: Build package in-place with coverage analysis enabled
run: |
pip3 install --editable .
env:
FLITTER_BUILD_COVERAGE: 1
- name: Run tests with coverage
- name: Install coverage and pytest
run: |
python -m pip install cython coverage pytest
- name: Install Linux Mesa OpenGL drivers and Microsoft fonts
run: |
sudo apt-get update
echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | sudo debconf-set-selections
sudo apt-get install -y mesa-utils xvfb libfontconfig1 ttf-mscorefonts-installer
- name: Run all tests with coverage
run: |
xvfb-run coverage run --source=src -m pytest tests --ignore=tests/test_engine.py --durations=5
- name: Generate coverage reports
Expand Down
17 changes: 6 additions & 11 deletions docs/canvas3d.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,11 +833,11 @@ To load an external model use the attributes:
will be automatically reloaded if this file changes.

`repair=` ( `true` | `false` )
: If set to `true`, attempts to *repair* the mesh by merging vertices and
removing duplicate or degenerate faces, fixing normal directions and face
windings, and capping holes. This can be useful if a mesh is rendering
incorrectly or is failing with [constructive solid
geometry](#contructive-solid-geometry) operations. Default is `false`.
: If set to `true`, attempts to *repair* the mesh by merging duplicated vertices
removing duplicate or degenerate faces, and fixing normal directions and face
windings. This can be useful if a loaded mesh is rendering incorrectly or is
failing with [constructive solid geometry](#contructive-solid-geometry)
operations. Default is `false`.

Meshes are loaded using the [**trimesh**](https://trimesh.org) library and so
**Flitter** supports all of the file-types supported by that, which includes
Expand Down Expand Up @@ -882,7 +882,7 @@ will have the same number of faces, but three distinct vertices per face.

For finer-grained control over shading, there is an edge snapping algorithm
that will take a smooth-shaded model, find sharp edges and split them into
seams. This algorithm can be controlled with the following attributes:
seams. This algorithm can be controlled with the following attribute:

`snap_edges=` `0``0.5`
: This specifies the minimum edge angle (in *turns*) at which to snap. It
Expand All @@ -892,11 +892,6 @@ mean that they are at right angles to one another. Specifying `0.5` will disable
the algorithm completely, `0` will cause all edges to be snapped (which is
equivalent to specifying `flat=true`).

`minimum_area=` `0``1`
: This specifies a minimum area for a face below which it will be ignored by the
algorithm. This is given as a ratio of face area to total model area. If not
specified, then all faces will be considered.

A model can also be *inverted* with the attribute:

`invert=` [ `true` | `false` ]
Expand Down
35 changes: 19 additions & 16 deletions src/flitter/engine/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,16 @@ async def run(self):
execution = render = housekeeping = 0
slow_frame = False
performance = 1 if self.realtime else 2
gc_pending = False
last_gc = None
run_program = current_program = errors = logs = None
simplify_state_time = system_clock() + self.state_simplify_wait
static = dict(self.defined_names)
static.update({'realtime': self.realtime, 'screen': self.screen, 'fullscreen': self.fullscreen,
'vsync': self.vsync, 'offscreen': self.offscreen, 'opengl_es': self.opengl_es, 'run_time': self.run_time})
gc.disable()
housekeeping -= system_clock()
while self.run_time is None or int(round((frame_time - start_time) * self.target_fps)) < int(round(self.run_time * self.target_fps)):
housekeeping -= system_clock()

beat = self.counter.beat_at_time(frame_time)
delta = beat - last
last = beat
Expand Down Expand Up @@ -228,9 +229,6 @@ async def run(self):
render += now
housekeeping -= now

del context
SharedCache.clean()

self.state['_counter'] = self.counter.tempo, self.counter.quantum, self.counter.start

if self.state.changed:
Expand Down Expand Up @@ -294,6 +292,18 @@ async def run(self):
for renderer in renderers:
await renderer.purge()

del context
SharedCache.clean()
gc_pending |= Model.flush_caches()
if gc_pending and (last_gc is None or now > last_gc + 1):
count = gc.collect(2)
gc_pending = False
last_gc = now
if count:
logger.trace("Collected {} objects (full collection)", count)
else:
gc.collect(0)

frame_count += 1
frames.append(frame_time if self.realtime else now)
frame_period = now - frame_time
Expand All @@ -313,25 +323,18 @@ async def run(self):
await asyncio.sleep(0)
frame_time = system_clock()

now = system_clock()
housekeeping += now

if len(frames) > 1 and frames[-1] - frames[0] > 5:
now = system_clock()
nframes = len(frames) - 1
fps = nframes / (frames[-1] - frames[0])
logger.info("{:4.1f}fps; {:4.1f}/{:4.1f}/{:4.1f}ms (run/render/sys); perf {:.2f}",
fps, 1000 * execution / nframes, 1000 * render / nframes, 1000 * housekeeping / nframes, performance)
fps, 1000 * execution / nframes, 1000 * render / nframes, 1000 * (housekeeping + now) / nframes, performance)
frames = frames[-1:]
execution = render = housekeeping = 0
execution = render = 0
housekeeping = -now
logger.trace("State dictionary size: {} keys", len(self.state))
if run_program is not None and run_program.stack is not None:
logger.trace("VM stack size: {:d}", run_program.stack.size)
if Model.flush_caches():
count = gc.collect(2)
if count:
logger.trace("Collected {} objects (full collection)", count)
else:
gc.collect(0)

finally:
self.global_state = {}
Expand Down
4 changes: 2 additions & 2 deletions src/flitter/render/window/canvas3d.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -510,8 +510,8 @@ cdef Model get_model(Node node, bint top):
if top:
if node.get_bool('flat', False):
model = model.flatten()
elif (snap_angle := node.get_float('snap_edges', DefaultSnapAngle if model.is_smooth() else 0.5)) < 0.5:
model = model._snap_edges(snap_angle, node.get_float('minimum_area', 0))
elif (snap_angle := node.get_float('snap_edges', DefaultSnapAngle if model.is_manifold() else 0.5)) < 0.5:
model = model._snap_edges(snap_angle)
if node.get_bool('invert', False):
model = model.invert()
if (mapping := node.get_str('uv_remap', None)) is not None:
Expand Down
9 changes: 7 additions & 2 deletions src/flitter/render/window/models.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ cdef double DefaultSnapAngle
cdef int64_t DefaultSegments


cpdef void fill_in_normals(vertices_array, faces_array)


cdef class Model:
cdef readonly uint64_t id
cdef readonly double touch_timestamp
Expand All @@ -19,23 +22,25 @@ cdef class Model:
cpdef bint uncache(self, bint buffers)
cpdef void unload(self)
cpdef void check_for_changes(self)
cpdef bint is_smooth(self)
cpdef bint is_manifold(self)
cpdef double signed_distance(self, double x, double y, double z) noexcept
cpdef tuple build_arrays(self)
cpdef object build_trimesh(self)
cpdef object build_manifold(self)

cpdef void add_dependent(self, Model model)
cpdef void remove_dependent(self, Model model)
cpdef void invalidate(self)
cpdef Vector get_bounds(self)
cpdef tuple get_arrays(self)
cpdef object get_trimesh(self)
cpdef object get_manifold(self)
cpdef tuple get_buffers(self, object glctx, dict objects)

cpdef Model flatten(self)
cpdef Model invert(self)
cpdef Model repair(self)
cdef Model _snap_edges(self, double snap_angle, double minimum_area)
cdef Model _snap_edges(self, double snap_angle)
cdef Model _transform(self, Matrix44 transform_matrix)
cdef Model _uv_remap(self, str mapping)
cdef Model _trim(self, Vector origin, Vector normal, double smooth, double fillet, double chamfer)
Expand Down
Loading

0 comments on commit 8d509a8

Please sign in to comment.