Skip to content

Commit 8ae174f

Browse files
authored
Refactor inter-context-canvas api (#21)
1 parent a8ddf97 commit 8ae174f

29 files changed

+1266
-191
lines changed

README.md

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ One canvas API, multiple backends 🚀
1212
<img width=354 src='https://github.com/user-attachments/assets/af8eefe0-4485-4daf-9fbd-36710e44f07c' />
1313
</div>
1414

15+
*This project is part of [pygfx.org](https://pygfx.org)*
16+
1517

1618
## Introduction
1719

@@ -33,7 +35,7 @@ same to the code that renders to them. Yet, the GUI systems are very different
3335

3436
The main use-case is rendering with [wgpu](https://github.com/pygfx/wgpu-py),
3537
but ``rendercanvas``can be used by anything that can render based on a window-id or
36-
by producing rgba images.
38+
by producing bitmap images.
3739

3840

3941
## Installation
@@ -51,18 +53,56 @@ pip install rendercanvas glfw
5153

5254
Also see the [online documentation](https://rendercanvas.readthedocs.io) and the [examples](https://github.com/pygfx/rendercanvas/tree/main/examples).
5355

56+
A minimal example that renders noise:
57+
```py
58+
import numpy as np
59+
from rendercanvas.auto import RenderCanvas, loop
60+
61+
canvas = RenderCanvas(update_mode="continuous")
62+
context = canvas.get_context("bitmap")
63+
64+
@canvas.request_draw
65+
def animate():
66+
w, h = canvas.get_logical_size()
67+
bitmap = np.random.uniform(0, 255, (h, w)).astype(np.uint8)
68+
context.set_bitmap(bitmap)
69+
70+
loop.run()
71+
```
72+
73+
Run wgpu visualizations:
5474
```py
55-
# Select either the glfw, qt or jupyter backend
5675
from rendercanvas.auto import RenderCanvas, loop
76+
from rendercanvas.utils.cube import setup_drawing_sync
77+
78+
79+
canvas = RenderCanvas(
80+
title="The wgpu cube example on $backend", update_mode="continuous"
81+
)
82+
draw_frame = setup_drawing_sync(canvas)
83+
canvas.request_draw(draw_frame)
84+
85+
loop.run()
86+
````
87+
88+
Embed in a Qt application:
89+
```py
90+
from PySide6 import QtWidgets
91+
from rendercanvas.qt import QRenderWidget
92+
93+
class Main(QtWidgets.QWidget):
5794

58-
# Visualizations can be embedded as a widget in a Qt application.
59-
# Supported qt libs are PySide6, PyQt6, PySide2 or PyQt5.
60-
from rendercanvas.pyside6 import QRenderWidget
95+
def __init__(self):
96+
super().__init__()
6197

98+
splitter = QtWidgets.QSplitter()
99+
self.canvas = QRenderWidget(splitter)
100+
...
62101

63-
# Now specify what the canvas should do on a draw
64-
TODO
65102

103+
app = QtWidgets.QApplication([])
104+
main = Main()
105+
app.exec()
66106
```
67107

68108

docs/advanced.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Advanced
2+
========
3+
4+
.. toctree::
5+
:maxdepth: 2
6+
:caption: Contents:
7+
8+
backendapi
9+
contextapi

docs/backendapi.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Internal backend API
2-
====================
1+
How backends work
2+
=================
33

44
This page documents what's needed to implement a backend for ``rendercanvas``. The purpose of this documentation is
55
to help maintain current and new backends. Making this internal API clear helps understanding how the backend-system works.

docs/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
# Load wglibu so autodoc can query docstrings
2424
import rendercanvas # noqa: E402
2525
import rendercanvas.stub # noqa: E402 - we use the stub backend to generate doccs
26-
26+
import rendercanvas._context # noqa: E402 - we use the ContexInterface to generate doccs
27+
import rendercanvas.utils.bitmappresentadapter # noqa: E402
2728

2829
# -- Project information -----------------------------------------------------
2930

docs/contextapi.rst

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
How context objects work
2+
========================
3+
4+
This page documents the working bentween the ``RenderCanvas`` and the context object.
5+
6+
7+
Introduction
8+
------------
9+
10+
The process of rendering to a canvas can be separated in two parts: *rendering*
11+
and *presenting*. The role of the context is to facilitate the rendering, and to
12+
then present the result to the screen. For this, the canvas provides one or more
13+
*present-methods*. Each canvas backend must provide at least the 'screen' or
14+
'bitmap' present-method.
15+
16+
.. code-block::
17+
18+
Rendering Presenting
19+
20+
┌─────────┐ ┌────────┐
21+
│ │ ──screen──► │ │
22+
──render──► | Context │ or │ Canvas │
23+
│ │ ──bitmap──► │ │
24+
└─────────┘ └────────┘
25+
26+
This means that for the context to be able to present to any canvas, it must
27+
support *both* the 'image' and 'screen' present-methods. If the context prefers
28+
presenting to the screen, and the canvas supports that, all is well. Similarly,
29+
if the context has a bitmap to present, and the canvas supports the
30+
bitmap-method, there's no problem.
31+
32+
It get's a little trickier when there's a mismatch, but we can deal with these
33+
cases too. When the context prefers presenting to screen, the rendered result is
34+
probably a texture on the GPU. This texture must then be downloaded to a bitmap
35+
on the CPU. All GPU API's have ways to do this.
36+
37+
.. code-block::
38+
39+
┌─────────┐ ┌────────┐
40+
│ │ ──tex─┐ │ │
41+
──render──► | Context │ | │ Canvas │
42+
│ │ └─bitmap──► │ |
43+
└─────────┘ └────────┘
44+
download from gpu to cpu
45+
46+
If the context has a bitmap to present, and the canvas only supports presenting
47+
to screen, you can usse a small utility: the ``BitmapPresentAdapter`` takes a
48+
bitmap and presents it to the screen.
49+
50+
.. code-block::
51+
52+
┌─────────┐ ┌────────┐
53+
│ │ ┌─screen──► │ │
54+
──render──► | Context │ │ │ Canvas │
55+
│ │ ──bitmap─┘ │ |
56+
└─────────┘ └────────┘
57+
use BitmapPresentAdapter
58+
59+
This way, contexts can be made to work with all canvas backens.
60+
61+
Canvases may also provide additionaly present-methods. If a context knows how to
62+
use that present-method, it can make use of it. Examples could be presenting
63+
diff images or video streams.
64+
65+
.. code-block::
66+
67+
┌─────────┐ ┌────────┐
68+
│ │ │ │
69+
──render──► | Context │ ──special-present-method──► │ Canvas │
70+
│ │ │ |
71+
└─────────┘ └────────┘
72+
73+
74+
Context detection
75+
-----------------
76+
77+
Anyone can make a context that works with ``rendercanvas``. In order for ``rendercanvas`` to find, it needs a little hook.
78+
79+
.. autofunction:: rendercanvas._context.rendercanvas_context_hook
80+
:no-index:
81+
82+
83+
Context API
84+
-----------
85+
86+
The class below describes the API and behavior that is expected of a context object.
87+
Also see https://github.com/pygfx/rendercanvas/blob/main/rendercanvas/_context.py.
88+
89+
.. autoclass:: rendercanvas._context.ContextInterface
90+
:members:
91+
:no-index:
92+
93+
94+
Adapter
95+
-------
96+
97+
.. autoclass:: rendercanvas.utils.bitmappresentadapter.BitmapPresentAdapter
98+
:members:
99+
:no-index:

docs/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Welcome to the rendercanvas docs!
1010
start
1111
api
1212
backends
13-
backendapi
13+
utils
14+
advanced
1415

1516

1617
Indices and tables

docs/start.rst

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ Since most users will want to render something to screen, we recommend installin
1919
pip install rendercanvas glfw
2020
2121
22-
Backends
23-
--------
24-
2522
Multiple backends are supported, including multiple GUI libraries, but none of these are installed by default. See :doc:`backends` for details.
2623

2724

@@ -36,6 +33,8 @@ In general, it's easiest to let ``rendercanvas`` select a backend automatically:
3633
3734
canvas = RenderCanvas()
3835
36+
# ... code to setup the rendering
37+
3938
loop.run() # Enter main-loop
4039
4140
@@ -44,11 +43,32 @@ Rendering to the canvas
4443

4544
The above just shows a grey window. We want to render to it by using wgpu or by generating images.
4645

47-
This API is still in flux at the moment. TODO
46+
Depending on the tool you'll use to render to the canvas, you need a different context.
47+
The purpose of the context to present the rendered result to the canvas.
48+
There are currently two types of contexts.
49+
50+
Rendering using bitmaps:
4851

4952
.. code-block:: py
5053
51-
present_context = canvas.get_context("wgpu")
54+
context = canvas.get_context("bitmap")
55+
56+
@canvas.request_draw
57+
def animate():
58+
# ... produce an image, represented with e.g. a numpy array
59+
context.set_bitmap(image)
60+
61+
Rendering with wgpu:
62+
63+
.. code-block:: py
64+
65+
context = canvas.get_context("wgpu")
66+
context.configure(device)
67+
68+
@canvas.request_draw
69+
def animate():
70+
texture = context.get_current_texture()
71+
# ... wgpu code
5272
5373
5474
Freezing apps

docs/utils.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Utils
2+
=====
3+
4+
.. toctree::
5+
:maxdepth: 2
6+
:caption: Contents:
7+
8+
utils_cube
9+
utils_bitmappresentadapter.rst
10+
utils_bitmaprenderingcontext.rst

docs/utils_bitmappresentadapter.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Bitmap present adapter
2+
======================
3+
4+
.. automodule:: rendercanvas.utils.bitmappresentadapter
5+
:members:

docs/utils_bitmaprenderingcontext.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Bitmap rendering context
2+
========================
3+
4+
.. automodule:: rendercanvas.utils.bitmaprenderingcontext
5+
:members:

docs/utils_cube.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Code for wgpu cube example
2+
==========================
3+
4+
.. automodule:: rendercanvas.utils.cube
5+
:members:

examples/noise.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""
2+
Simple example that uses the bitmap-context to show images of noise.
3+
"""
4+
5+
import numpy as np
6+
from rendercanvas.auto import RenderCanvas, loop
7+
8+
9+
canvas = RenderCanvas(update_mode="continuous")
10+
context = canvas.get_context("bitmap")
11+
12+
13+
@canvas.request_draw
14+
def animate():
15+
w, h = canvas.get_logical_size()
16+
shape = int(h) // 4, int(w) // 4
17+
18+
bitmap = np.random.uniform(0, 255, shape).astype(np.uint8)
19+
context.set_bitmap(bitmap)
20+
21+
22+
loop.run()

examples/snake.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""
2+
Simple snake game based on bitmap rendering. Work in progress.
3+
"""
4+
5+
from collections import deque
6+
7+
import numpy as np
8+
9+
from rendercanvas.auto import RenderCanvas, loop
10+
11+
12+
canvas = RenderCanvas(present_method=None, update_mode="continuous")
13+
14+
context = canvas.get_context("bitmap")
15+
16+
world = np.zeros((120, 160), np.uint8)
17+
pos = [100, 100]
18+
direction = [1, 0]
19+
q = deque()
20+
21+
22+
@canvas.add_event_handler("key_down")
23+
def on_key(event):
24+
key = event["key"]
25+
if key == "ArrowLeft":
26+
direction[0] = -1
27+
direction[1] = 0
28+
elif key == "ArrowRight":
29+
direction[0] = 1
30+
direction[1] = 0
31+
elif key == "ArrowUp":
32+
direction[0] = 0
33+
direction[1] = -1
34+
elif key == "ArrowDown":
35+
direction[0] = 0
36+
direction[1] = 1
37+
38+
39+
@canvas.request_draw
40+
def animate():
41+
pos[0] += direction[0]
42+
pos[1] += direction[1]
43+
44+
if pos[0] < 0:
45+
pos[0] = world.shape[1] - 1
46+
elif pos[0] >= world.shape[1]:
47+
pos[0] = 0
48+
if pos[1] < 0:
49+
pos[1] = world.shape[0] - 1
50+
elif pos[1] >= world.shape[0]:
51+
pos[1] = 0
52+
53+
q.append(tuple(pos))
54+
world[pos[1], pos[0]] = 255
55+
56+
while len(q) > 20:
57+
old_pos = q.popleft()
58+
world[old_pos[1], old_pos[0]] = 0
59+
60+
context.set_bitmap(world)
61+
62+
63+
loop.run()

examples/wx_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(self):
1717

1818
# Using present_method 'image' because it reports "The surface texture is suboptimal"
1919
self.canvas = RenderWidget(
20-
self, update_mode="continuous", present_method="image"
20+
self, update_mode="continuous", present_method="bitmap"
2121
)
2222
self.button = wx.Button(self, -1, "Hello world")
2323
self.output = wx.StaticText(self)

0 commit comments

Comments
 (0)