Skip to content

Commit

Permalink
Refactor inter-context-canvas api (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
almarklein authored Nov 18, 2024
1 parent a8ddf97 commit 8ae174f
Show file tree
Hide file tree
Showing 29 changed files with 1,266 additions and 191 deletions.
54 changes: 47 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ One canvas API, multiple backends 🚀
<img width=354 src='https://github.com/user-attachments/assets/af8eefe0-4485-4daf-9fbd-36710e44f07c' />
</div>

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


## Introduction

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

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


## Installation
Expand All @@ -51,18 +53,56 @@ pip install rendercanvas glfw

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

A minimal example that renders noise:
```py
import numpy as np
from rendercanvas.auto import RenderCanvas, loop

canvas = RenderCanvas(update_mode="continuous")
context = canvas.get_context("bitmap")

@canvas.request_draw
def animate():
w, h = canvas.get_logical_size()
bitmap = np.random.uniform(0, 255, (h, w)).astype(np.uint8)
context.set_bitmap(bitmap)

loop.run()
```

Run wgpu visualizations:
```py
# Select either the glfw, qt or jupyter backend
from rendercanvas.auto import RenderCanvas, loop
from rendercanvas.utils.cube import setup_drawing_sync


canvas = RenderCanvas(
title="The wgpu cube example on $backend", update_mode="continuous"
)
draw_frame = setup_drawing_sync(canvas)
canvas.request_draw(draw_frame)

loop.run()
````

Embed in a Qt application:
```py
from PySide6 import QtWidgets
from rendercanvas.qt import QRenderWidget

class Main(QtWidgets.QWidget):

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

splitter = QtWidgets.QSplitter()
self.canvas = QRenderWidget(splitter)
...

# Now specify what the canvas should do on a draw
TODO

app = QtWidgets.QApplication([])
main = Main()
app.exec()
```


Expand Down
9 changes: 9 additions & 0 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Advanced
========

.. toctree::
:maxdepth: 2
:caption: Contents:

backendapi
contextapi
4 changes: 2 additions & 2 deletions docs/backendapi.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Internal backend API
====================
How backends work
=================

This page documents what's needed to implement a backend for ``rendercanvas``. The purpose of this documentation is
to help maintain current and new backends. Making this internal API clear helps understanding how the backend-system works.
Expand Down
3 changes: 2 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
# Load wglibu so autodoc can query docstrings
import rendercanvas # noqa: E402
import rendercanvas.stub # noqa: E402 - we use the stub backend to generate doccs

import rendercanvas._context # noqa: E402 - we use the ContexInterface to generate doccs
import rendercanvas.utils.bitmappresentadapter # noqa: E402

# -- Project information -----------------------------------------------------

Expand Down
99 changes: 99 additions & 0 deletions docs/contextapi.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
How context objects work
========================

This page documents the working bentween the ``RenderCanvas`` and the context object.


Introduction
------------

The process of rendering to a canvas can be separated in two parts: *rendering*
and *presenting*. The role of the context is to facilitate the rendering, and to
then present the result to the screen. For this, the canvas provides one or more
*present-methods*. Each canvas backend must provide at least the 'screen' or
'bitmap' present-method.

.. code-block::
Rendering Presenting
┌─────────┐ ┌────────┐
│ │ ──screen──► │ │
──render──► | Context │ or │ Canvas │
│ │ ──bitmap──► │ │
└─────────┘ └────────┘
This means that for the context to be able to present to any canvas, it must
support *both* the 'image' and 'screen' present-methods. If the context prefers
presenting to the screen, and the canvas supports that, all is well. Similarly,
if the context has a bitmap to present, and the canvas supports the
bitmap-method, there's no problem.

It get's a little trickier when there's a mismatch, but we can deal with these
cases too. When the context prefers presenting to screen, the rendered result is
probably a texture on the GPU. This texture must then be downloaded to a bitmap
on the CPU. All GPU API's have ways to do this.

.. code-block::
┌─────────┐ ┌────────┐
│ │ ──tex─┐ │ │
──render──► | Context │ | │ Canvas │
│ │ └─bitmap──► │ |
└─────────┘ └────────┘
download from gpu to cpu
If the context has a bitmap to present, and the canvas only supports presenting
to screen, you can usse a small utility: the ``BitmapPresentAdapter`` takes a
bitmap and presents it to the screen.

.. code-block::
┌─────────┐ ┌────────┐
│ │ ┌─screen──► │ │
──render──► | Context │ │ │ Canvas │
│ │ ──bitmap─┘ │ |
└─────────┘ └────────┘
use BitmapPresentAdapter
This way, contexts can be made to work with all canvas backens.

Canvases may also provide additionaly present-methods. If a context knows how to
use that present-method, it can make use of it. Examples could be presenting
diff images or video streams.

.. code-block::
┌─────────┐ ┌────────┐
│ │ │ │
──render──► | Context │ ──special-present-method──► │ Canvas │
│ │ │ |
└─────────┘ └────────┘
Context detection
-----------------

Anyone can make a context that works with ``rendercanvas``. In order for ``rendercanvas`` to find, it needs a little hook.

.. autofunction:: rendercanvas._context.rendercanvas_context_hook
:no-index:


Context API
-----------

The class below describes the API and behavior that is expected of a context object.
Also see https://github.com/pygfx/rendercanvas/blob/main/rendercanvas/_context.py.

.. autoclass:: rendercanvas._context.ContextInterface
:members:
:no-index:


Adapter
-------

.. autoclass:: rendercanvas.utils.bitmappresentadapter.BitmapPresentAdapter
:members:
:no-index:
3 changes: 2 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Welcome to the rendercanvas docs!
start
api
backends
backendapi
utils
advanced


Indices and tables
Expand Down
30 changes: 25 additions & 5 deletions docs/start.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ Since most users will want to render something to screen, we recommend installin
pip install rendercanvas glfw
Backends
--------

Multiple backends are supported, including multiple GUI libraries, but none of these are installed by default. See :doc:`backends` for details.


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

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

This API is still in flux at the moment. TODO
Depending on the tool you'll use to render to the canvas, you need a different context.
The purpose of the context to present the rendered result to the canvas.
There are currently two types of contexts.

Rendering using bitmaps:

.. code-block:: py
present_context = canvas.get_context("wgpu")
context = canvas.get_context("bitmap")
@canvas.request_draw
def animate():
# ... produce an image, represented with e.g. a numpy array
context.set_bitmap(image)
Rendering with wgpu:

.. code-block:: py
context = canvas.get_context("wgpu")
context.configure(device)
@canvas.request_draw
def animate():
texture = context.get_current_texture()
# ... wgpu code
Freezing apps
Expand Down
10 changes: 10 additions & 0 deletions docs/utils.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Utils
=====

.. toctree::
:maxdepth: 2
:caption: Contents:

utils_cube
utils_bitmappresentadapter.rst
utils_bitmaprenderingcontext.rst
5 changes: 5 additions & 0 deletions docs/utils_bitmappresentadapter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Bitmap present adapter
======================

.. automodule:: rendercanvas.utils.bitmappresentadapter
:members:
5 changes: 5 additions & 0 deletions docs/utils_bitmaprenderingcontext.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Bitmap rendering context
========================

.. automodule:: rendercanvas.utils.bitmaprenderingcontext
:members:
5 changes: 5 additions & 0 deletions docs/utils_cube.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Code for wgpu cube example
==========================

.. automodule:: rendercanvas.utils.cube
:members:
22 changes: 22 additions & 0 deletions examples/noise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
Simple example that uses the bitmap-context to show images of noise.
"""

import numpy as np
from rendercanvas.auto import RenderCanvas, loop


canvas = RenderCanvas(update_mode="continuous")
context = canvas.get_context("bitmap")


@canvas.request_draw
def animate():
w, h = canvas.get_logical_size()
shape = int(h) // 4, int(w) // 4

bitmap = np.random.uniform(0, 255, shape).astype(np.uint8)
context.set_bitmap(bitmap)


loop.run()
63 changes: 63 additions & 0 deletions examples/snake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Simple snake game based on bitmap rendering. Work in progress.
"""

from collections import deque

import numpy as np

from rendercanvas.auto import RenderCanvas, loop


canvas = RenderCanvas(present_method=None, update_mode="continuous")

context = canvas.get_context("bitmap")

world = np.zeros((120, 160), np.uint8)
pos = [100, 100]
direction = [1, 0]
q = deque()


@canvas.add_event_handler("key_down")
def on_key(event):
key = event["key"]
if key == "ArrowLeft":
direction[0] = -1
direction[1] = 0
elif key == "ArrowRight":
direction[0] = 1
direction[1] = 0
elif key == "ArrowUp":
direction[0] = 0
direction[1] = -1
elif key == "ArrowDown":
direction[0] = 0
direction[1] = 1


@canvas.request_draw
def animate():
pos[0] += direction[0]
pos[1] += direction[1]

if pos[0] < 0:
pos[0] = world.shape[1] - 1
elif pos[0] >= world.shape[1]:
pos[0] = 0
if pos[1] < 0:
pos[1] = world.shape[0] - 1
elif pos[1] >= world.shape[0]:
pos[1] = 0

q.append(tuple(pos))
world[pos[1], pos[0]] = 255

while len(q) > 20:
old_pos = q.popleft()
world[old_pos[1], old_pos[0]] = 0

context.set_bitmap(world)


loop.run()
2 changes: 1 addition & 1 deletion examples/wx_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def __init__(self):

# Using present_method 'image' because it reports "The surface texture is suboptimal"
self.canvas = RenderWidget(
self, update_mode="continuous", present_method="image"
self, update_mode="continuous", present_method="bitmap"
)
self.button = wx.Button(self, -1, "Hello world")
self.output = wx.StaticText(self)
Expand Down
Loading

0 comments on commit 8ae174f

Please sign in to comment.