Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
a22815e
Use buffer mapping instead of queue.read_texture
almarklein Nov 11, 2025
540c2d8
bitmap async by lagging one frame
almarklein Nov 11, 2025
e8456fe
delete files I accidentally added
almarklein Nov 12, 2025
eff9217
Merge branch 'main' into async-bitmap
almarklein Nov 14, 2025
c8ac9a8
add note
almarklein Nov 14, 2025
cccdb42
Refactor to implement basic ring buffer
almarklein Nov 14, 2025
7fafc89
polishing a bit
almarklein Nov 14, 2025
6dbb1e1
Re-implement precise sleep
almarklein Nov 17, 2025
ecd3804
improve accuracy of raw loop
almarklein Nov 17, 2025
b91a4ac
ruff
almarklein Nov 17, 2025
39903dc
docs
almarklein Nov 17, 2025
dd3b330
Try a scheduler thread util
almarklein Nov 18, 2025
2b74282
improvinh
almarklein Nov 18, 2025
8554cda
Also apply for trio
almarklein Nov 18, 2025
493924e
tiny tweak
almarklein Nov 18, 2025
aba63cc
Implement for wx
almarklein Nov 19, 2025
2591017
Make precise timers and threaded timers work for all qt backends
almarklein Nov 19, 2025
19aa7a4
Clean up
almarklein Nov 19, 2025
9a01953
add comment
almarklein Nov 19, 2025
6270abb
simplify thread code a bit
almarklein Nov 19, 2025
c06c5c5
Avoid using Future.set_result, which we are not supposed to be calling
almarklein Nov 19, 2025
5326bab
clean
almarklein Nov 19, 2025
eeabd01
comment
almarklein Nov 19, 2025
8f6bbda
Using the thread, the raw loop can become dead simple
almarklein Nov 19, 2025
886e6d4
cleanup
almarklein Nov 19, 2025
f1b89fb
Merge branch 'main' into precise-sleep
almarklein Nov 20, 2025
7602bba
Merge branch 'precise-sleep' into async-bitmap
almarklein Nov 20, 2025
8b12050
Merge branch 'main' into async-bitmap
almarklein Dec 17, 2025
d7f5ba6
some textual changes
almarklein Dec 17, 2025
78fc1ef
Merge branch 'main' into async-bitmap
almarklein Dec 23, 2025
550c7f5
bitmap present can go outside of animation frame
almarklein Dec 24, 2025
5a72bc1
It's messy, but its working. Will cleanup after xmas
almarklein Dec 24, 2025
8d8c9c2
Minor cleanup
almarklein Jan 9, 2026
fe99943
comment
almarklein Jan 9, 2026
85a6de1
Offscreen.draw is sync, and so is force_draw()
almarklein Jan 12, 2026
a3361ab
Merge branch 'main' into async-bitmap
almarklein Jan 15, 2026
167c05b
Merge branch 'main' into async-bitmap
almarklein Jan 19, 2026
51550ab
progress
almarklein Jan 23, 2026
cb48dbe
Some cleanuo
almarklein Jan 23, 2026
a271969
fix for pyqt5
almarklein Jan 23, 2026
d0aefc8
Fix wx bitmap present
almarklein Jan 23, 2026
604e59c
Merge branch 'main' into async-bitmap
almarklein Jan 23, 2026
819b8c0
fix force-draw for qt and wx on macos
almarklein Jan 23, 2026
697608c
Better moment to process events to reduce delay
almarklein Jan 23, 2026
0d3f78c
cleanup
almarklein Jan 23, 2026
3c7368a
restore example
almarklein Jan 23, 2026
cf0d40a
fix for pyqt6
almarklein Jan 23, 2026
e799cdd
Tweak api a bit
almarklein Jan 26, 2026
7286e95
Just one downloader
almarklein Jan 26, 2026
5ab755c
Clean up low-level presentation
almarklein Jan 27, 2026
6abd8c1
fixes
almarklein Jan 27, 2026
fb40c2f
Undo earlier change
almarklein Jan 27, 2026
d9007ee
Also use numpy in bitmap contexts
almarklein Jan 27, 2026
0e540ba
minor tweaks
almarklein Jan 27, 2026
03b6345
fix
almarklein Jan 27, 2026
9392644
Apply suggestions from code review
almarklein Jan 28, 2026
b6bb81a
add version checks for wgpu
almarklein Jan 30, 2026
3d0510f
Fix flicker on qt
almarklein Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ keywords = [
"jupyter",
]
requires-python = ">= 3.10"
dependencies = [] # Zero hard dependencies!
dependencies = ['numpy'] # Numpy is the only hard dependency
[project.optional-dependencies]
# For users
jupyter = ["jupyter_rfb>=0.4.2"]
Expand Down
80 changes: 53 additions & 27 deletions rendercanvas/_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ def __init__(
# ... = canvas.get_context() -> No, context creation should be lazy!

# Scheduling variables
self.set_enabled(True)
self.set_update_mode(update_mode, min_fps=min_fps, max_fps=max_fps)
self._draw_requested = True # Start with a draw in ondemand mode
self._async_draw_event = None
self._ready_for_present = None

# Keep track of fps
self._draw_stats = 0, time.perf_counter()
Expand Down Expand Up @@ -92,6 +93,11 @@ def set_update_mode(self, update_mode, *, min_fps=None, max_fps=None):
)
self._max_fps = max(1, float(max_fps))

def set_enabled(self, enabled: bool):
self._enabled = bool(enabled)
if self._enabled:
self._draw_requested = True

def request_draw(self):
"""Request a new draw to be done. Only affects the 'ondemand' mode."""
# Just set the flag
Expand All @@ -108,7 +114,9 @@ async def __scheduler_task(self):

while True:
# Determine delay
if self._mode == "fastest" or self._max_fps <= 0:
if not self._enabled:
delay = 0.1
elif self._mode == "fastest" or self._max_fps <= 0:
delay = 0
else:
delay = 1 / self._max_fps
Expand All @@ -129,7 +137,9 @@ async def __scheduler_task(self):

do_draw = False

if self._mode == "fastest":
if not self._enabled:
pass
elif self._mode == "fastest":
# fastest: draw continuously as fast as possible, ignoring fps settings.
do_draw = True
elif self._mode == "continuous":
Expand All @@ -150,45 +160,61 @@ async def __scheduler_task(self):
else:
raise RuntimeError(f"Unexpected scheduling mode: '{self._mode}'")

# Get canvas object or stop the loop
# Get canvas object or stop the loop. Make sure to delete this reference asap
if (canvas := self.get_canvas()) is None:
break

# Process events now.
# Note that we don't want to emit events *during* the draw, because event
# callbacks do stuff, and that stuff may include changing the canvas size,
# or affect layout in a UI application, all which are not recommended during
# the main draw-event (a.k.a. animation frame), and may even lead to errors.
# The one exception is resize events, which we do emit during a draw, if the
# size has changed since the last time that events were processed.
canvas._process_events()

if not do_draw:
# If we don't want to draw, move to the next iter
if do_draw:
# We do a draw and wait for the full draw to complete, including
# the presentation (i.e. the 'consumption' of the frame), using
# an async-event. This async-wait does not do much for the
# 'screen' present method, but for bitmap presenting is makes a
# huge difference, because the CPU can do other things while the
# GPU is downloading the frame. Benchmarks with the simple cube
# example indicate that its about twice as fast as sync-waiting.
#
# We could play with the positioning of where we wait for the
# event. E.g. we could draw the current frame and *then* wait
# for the previous frame to be presented, before initiating the
# presentation of the current frame. However, this does *not*
# seem to be faster! Tested on MacOS M1 and Windows with NVidia
# on the cube example.
#
# Moreover, even if this 'interleaving' of drawing and
# presentation can improve the FPS in some cases, it would be at
# the cost of the delay between processing of user-events and
# the presentation of the corresponding frame to the user. In
# terms of user-experience, the cost of this delay is probably
# larger than the benefit of the potential fps increase.

self._ready_for_present = Event()
canvas._rc_request_draw()
del canvas
continue
if self._ready_for_present:
await self._ready_for_present.wait()
else:
# Otherwise, request a draw ...
canvas._rc_request_draw()
# If we have a non-drawing tick, at least process events.
canvas._process_events()
del canvas
# ... and wait for the draw to happen
self._async_draw_event = Event()
await self._async_draw_event.wait()
last_draw_time = time.perf_counter()

# Note that when the canvas is closed, we may detect it here and break from the loop.
# But the task may also be waiting for a draw to happen, or something else. In that case
# this task will be cancelled when the loop ends. In any case, this is why this is not
# a good place to detect the canvas getting closed, the loop does this.

def on_draw(self):
# Bookkeeping
def on_cancel_draw(self):
if self._ready_for_present is not None:
self._ready_for_present.set()
self._ready_for_present = None

def on_about_to_draw(self):
self._draw_requested = False

def on_draw_done(self):
# Keep ticking
if self._async_draw_event:
self._async_draw_event.set()
self._async_draw_event = None
if self._ready_for_present is not None:
self._ready_for_present.set()
self._ready_for_present = None

# Update stats
count, last_time = self._draw_stats
Expand Down
Loading