Skip to content

Commit

Permalink
Refactor inter-context-canvas api
Browse files Browse the repository at this point in the history
  • Loading branch information
almarklein committed Nov 14, 2024
1 parent a8ddf97 commit a30bffa
Show file tree
Hide file tree
Showing 23 changed files with 911 additions and 186 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
====================
Backend API
===========

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
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# 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


# -- Project information -----------------------------------------------------
Expand Down
29 changes: 29 additions & 0 deletions docs/contextapi.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Context API
===========

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


Context detection
-----------------

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


Context
-------

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


RenderCanvas
------------

This shows the subset of methods of a canvas that relates to the context (see :doc:`backendapi` for the complete list).

.. autoclass:: rendercanvas.stub.StubRenderCanvas
:members: _rc_get_present_methods, get_context, get_physical_size, get_logical_size,
:no-index:
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Welcome to the rendercanvas docs!
start
api
backends
backendapi
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
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
78 changes: 78 additions & 0 deletions rendercanvas/_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
A stub context imlpementation for documentation purposes.
It does actually work, but presents nothing.
"""

import weakref


def rendercanvas_context_hook(canvas, present_methods):
"""Hook function to allow ``rendercanvas`` to detect your context implementation.
If you make a function with this name available in the module ``your.module``,
``rendercanvas`` will detect and call this function in order to obtain the canvas object.
That way, anyone can use ``canvas.get_context("your.module")`` to use your context.
The arguments are the same as for ``ContextInterface``.
"""
return ContextInterface(canvas, present_methods)


class ContextInterface:
"""The interface that a context must implement, to be usable with a ``RenderCanvas``.
Arguments:
canvas (BaseRenderCanvas): the canvas to render to.
present_methods (dict): The supported present methods of the canvas.
The ``present_methods`` dict has a field for each supported present-method. A
canvas must support either "screen" or "bitmap". It may support both, as well as
additional (specialized) present methods. Below we list the common methods and
what fields the subdicts have.
* Render method "screen":
* "window": the native window id.
* "display": the native display id (Linux only).
* "platform": to determine between "x11" and "wayland" (Linux only).
* Render method "bitmap":
* "formats": a list of supported formats. It should always include "rgba-u8".
Other options can be be "i-u8" (intensity/grayscale), "i-f32", "bgra-u8", "rgba-u16", etc.
"""

def __init__(self, canvas, present_methods):
self._canvas_ref = weakref.ref(canvas)
self._present_methods = present_methods

@property
def canvas(self):
"""The associated canvas object. Internally, this should preferably be stored using a weakref."""
return self._canvas_ref()

def present(self):
"""Present the result to the canvas.
This is called by the canvas, and should not be called by user-code.
The implementation should always return a present-result dict, which
should have at least a field 'method'. The value of 'method' must be
one of the methods that the canvas supports, i.e. it must be in ``present_methods``.
* If there is nothing to present, e.g. because nothing was rendered yet:
* return ``{"method": "skip"}`` (special case).
* If presentation could not be done for some reason:
* return ``{"method": "fail", "message": "xx"}`` (special case).
* If ``present_method`` is "screen":
* Render to screen using the info in ``present_methods['screen']``).
* Return ``{"method", "screen"}`` as confirmation.
* If ``present_method`` is "bitmap":
* Return ``{"method": "bitmap", "data": data, "format": format}``.
* 'data' is a memoryview, or something that can be converted to a memoryview, like a numpy array.
* 'format' is the format of the bitmap, must be in ``present_methods['bitmap']['formats']`` ("rgba-u8" is always supported).
* If ``present_method`` is something else:
* Return ``{"method": "xx", ...}``.
* It's the responsibility of the context to use a render method that is supported by the canvas,
and that the appropriate arguments are supplied.
"""

# This is a stub
return {"method": "skip"}
Loading

0 comments on commit a30bffa

Please sign in to comment.