diff --git a/docs/api.rst b/docs/api.rst index c6bdf34..63fc379 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,11 +1,25 @@ -rendercanvas base classes -========================= +API +=== -.. autoclass:: rendercanvas.base.BaseRenderCanvas +These are the base classes that make up the rendercanvas API: + +* The :class:`~rendercanvas.BaseRenderCanvas` represets the main API. +* The :class:`~rendercanvas.BaseLoop` provides functionality to work with the event-loop and timers in a generic way. +* The :class:`~rendercanvas.BaseTimer` is returned by some methods of ``loop``. +* The :class:`~rendercanvas.EventType` specifies the different types of events that can be connected to with :func:`canvas.add_event_handler() `. + +.. autoclass:: rendercanvas.BaseRenderCanvas + :members: + :member-order: bysource + +.. autoclass:: rendercanvas.BaseLoop :members: + :member-order: bysource -.. autoclass:: rendercanvas.base.BaseLoop +.. autoclass:: rendercanvas.BaseTimer :members: + :member-order: bysource -.. autoclass:: rendercanvas.base.BaseTimer +.. autoclass:: rendercanvas.EventType :members: + :member-order: bysource diff --git a/docs/backendapi.rst b/docs/backendapi.rst new file mode 100644 index 0000000..22d1672 --- /dev/null +++ b/docs/backendapi.rst @@ -0,0 +1,3 @@ +Backend API +=========== +TODO \ No newline at end of file diff --git a/docs/gui.rst b/docs/backends.rst similarity index 55% rename from docs/gui.rst rename to docs/backends.rst index 1513274..13f8d8b 100644 --- a/docs/gui.rst +++ b/docs/backends.rst @@ -1,58 +1,23 @@ -gui API -======= +Backends +======== -.. currentmodule:: rendercanvas - -You can use vanilla wgpu for compute tasks and to render offscreen. To -render to a window on screen we need a *canvas*. Since the Python -ecosystem provides many different GUI toolkits, rendercanvas implements a base -canvas class, and has builtin support for a few GUI toolkits. At the -moment these include GLFW, Jupyter, Qt, and wx. - - -The Canvas base classes ------------------------ - -For each supported GUI toolkit there is a module that implements a ``RenderCanvas`` class, -which inherits from :class:`BaseRenderCanvas`, providing a common API. -The GLFW, Qt, and Jupyter backends also inherit from :class:`WgpuAutoGui` to include -support for events (interactivity). In the next sections we demonstrates the different -canvas classes that you can use. - - -Events ------- - -To implement interaction with a ``RenderCanvas``, use the :func:`BaseRenderCanvas.add_event_handler()` method. -Events come in the following flavours: - -.. autoclass:: EventType - :members: - - -The auto GUI backend --------------------- +The auto backend +----------------- Generally the best approach for examples and small applications is to use the -automatically selected GUI backend. This ensures that the code is portable +automatically selected backend. This ensures that the code is portable across different machines and environments. Using ``rendercanvas.auto`` selects a suitable backend depending on the environment and more. See :ref:`interactive_use` for details. -To implement interaction, the ``canvas`` has a :func:`WgpuAutoGui.handle_event()` method -that can be overloaded. Alternatively you can use it's :func:`WgpuAutoGui.add_event_handler()` -method. See the `event spec `_ -for details about the event objects. - - .. code-block:: py - from wgpu.gui.auto import RenderCanvas, run, call_later + from rendercanvas.auto import RenderCanvas, loop canvas = RenderCanvas(title="Example") canvas.request_draw(your_draw_function) - run() + loop.run() Support for GLFW @@ -64,31 +29,29 @@ but you can replace ``from rendercanvas.auto`` with ``from rendercanvas.glfw`` t .. code-block:: py - from wgpu.gui.glfw import RenderCanvas, run, call_later + from rendercanvas.glfw import RenderCanvas, loop canvas = RenderCanvas(title="Example") canvas.request_draw(your_draw_function) - run() + loop.run() Support for Qt -------------- -There is support for PyQt5, PyQt6, PySide2 and PySide6. The rendercanvas library detects what -library you are using by looking what module has been imported. +RenderCanvas has support for PyQt5, PyQt6, PySide2 and PySide6. It detects what +qt library you are using by looking what module has been imported. For a toplevel widget, the ``rendercanvas.qt.RenderCanvas`` class can be imported. If you want to embed the canvas as a subwidget, use ``rendercanvas.qt.QRenderWidget`` instead. -Also see the `Qt triangle example `_ -and `Qt triangle embed example `_. - .. code-block:: py # Import any of the Qt libraries before importing the RenderCanvas. - # This way wgpu knows which Qt library to use. + # This way rendercanvas knows which Qt library to use. from PySide6 import QtWidgets - from wgpu.gui.qt import RenderCanvas + from rendercanvas.qt import RenderCanvas # use this for top-level windows + from rendercanvas.qt import QRenderWidget # use this for widgets in you application app = QtWidgets.QApplication([]) @@ -104,17 +67,15 @@ and `Qt triangle embed example `_ -and `wx triangle embed example `_. .. code-block:: py import wx - from wgpu.gui.wx import RenderCanvas + from rendercanvas.wx import RenderCanvas app = wx.App() @@ -137,7 +98,7 @@ object, but in some cases it's convenient to do so with a canvas-like API. .. code-block:: py - from wgpu.gui.offscreen import RenderCanvas + from rendercanvas.offscreen import RenderCanvas # Instantiate the canvas canvas = RenderCanvas(size=(500, 400), pixel_ratio=1) @@ -154,30 +115,30 @@ object, but in some cases it's convenient to do so with a canvas-like API. Support for Jupyter lab and notebook ------------------------------------ -WGPU can be used in Jupyter lab and the Jupyter notebook. This canvas +RenderCanvas can be used in Jupyter lab and the Jupyter notebook. This canvas is based on `jupyter_rfb `_, an ipywidget subclass implementing a remote frame-buffer. There are also some `wgpu examples `_. .. code-block:: py - # from wgpu.gui.jupyter import RenderCanvas # Direct approach - from wgpu.gui.auto import RenderCanvas # Approach compatible with desktop usage + # from rendercanvas.jupyter import RenderCanvas # Direct approach + from rendercanvas.auto import RenderCanvas # also works, because rendercanvas detects Jupyter canvas = RenderCanvas() - # ... wgpu code + # ... rendering code canvas # Use as cell output .. _interactive_use: -Using a canvas interactively ----------------------------- +Interactive use +--------------- -The rendercanvas gui's are designed to support interactive use. Firstly, this is -realized by automatically selecting the appropriate GUI backend. Secondly, the -``run()`` function (which normally enters the event-loop) does nothing in an +The rendercanvas backends are designed to support interactive use. Firstly, this is +realized by automatically selecting the appropriate backend. Secondly, the +``loop.run()`` method (which normally enters the event-loop) does nothing in an interactive session. Many interactive environments have some sort of GUI support, allowing the repl @@ -191,7 +152,7 @@ honor that and use Qt instead. On ``jupyter console`` and ``qtconsole``, the kernel is the same as in ``jupyter notebook``, making it (about) impossible to tell that we cannot actually use ipywidgets. So it will try to use ``jupyter_rfb``, but cannot render anything. -It's theefore advised to either use ``%gui qt`` or set the ``WGPU_GUI_BACKEND`` env var +It's therefore advised to either use ``%gui qt`` or set the ``WGPU_GUI_BACKEND`` env var to "glfw". The latter option works well, because these kernels *do* have a running asyncio event loop! @@ -204,7 +165,7 @@ On IPython (the old-school terminal app) it's advised to use ``%gui qt`` (or On IDE's like Spyder or Pyzo, rendercanvas detects the integrated GUI, running on glfw if asyncio is enabled or Qt if a qt app is running. -On an interactive session without GUI support, one must call ``run()`` to make +On an interactive session without GUI support, one must call ``loop.run()`` to make the canvases interactive. This enters the main loop, which prevents entering new code. Once all canvases are closed, the loop returns. If you make new canvases afterwards, you can call ``run()`` again. This is similar to ``plt.show()`` in Matplotlib. diff --git a/docs/guide.rst b/docs/guide.rst deleted file mode 100644 index 65617f6..0000000 --- a/docs/guide.rst +++ /dev/null @@ -1,255 +0,0 @@ -Guide (deprecated) -================== - -**TODO: REMOVE/REPLACE THIS** - -The ``wgpu`` library presents a Pythonic API for the `WebGPU spec -`_. It is an API to control graphics -hardware. Like OpenGL but modern. Or like Vulkan but higher level. -GPU programming is a craft that requires knowledge of how GPU's work. - - -Getting started ---------------- - -Creating a canvas -+++++++++++++++++ - -If you want to render to the screen, you need a canvas. Multiple -GUI toolkits are supported, see the :doc:`gui`. In general, it's easiest to let ``wgpu`` select a GUI automatically: - -.. code-block:: py - - from wgpu.gui.auto import RenderCanvas, run - - canvas = RenderCanvas(title="a wgpu example") - - -Next, we can setup the render context, which we will need later on. - -.. code-block:: py - - present_context = canvas.get_context() - render_texture_format = present_context.get_preferred_format(device.adapter) - present_context.configure(device=device, format=render_texture_format) - - -Obtaining a device -++++++++++++++++++ - -The next step is to obtain an adapter, which represents an abstract render device. -You can pass it the ``canvas`` that you just created, or pass ``None`` for the canvas -if you have none (e.g. for compute or offscreen rendering). From the adapter, -you can obtain a device. - -.. code-block:: py - - adapter = wgpu.gpu.request_adapter_sync(power_preference="high-performance") - device = adapter.request_device_sync() - -The ``wgpu.gpu`` object is the API entrypoint (:class:`wgpu.GPU`). It contains just a handful of functions, -including ``request_adapter()``. The device is used to create most other GPU objects. - - -Creating buffers, textures shaders, etc. -++++++++++++++++++++++++++++++++++++++++ - -Using the device, you can create buffers, textures, write shader code, and put -these together into pipeline objects. How to do this depends a lot on what you -want to achieve, and is therefore out of scope for this guide. Have a look at the examples -or some of the tutorials that we link to below. - -Setting up a draw function -++++++++++++++++++++++++++ - -Let's now define a function that will actually draw the stuff we put together in -the previous step. - -.. code-block:: py - - def draw_frame(): - - # We'll record commands that we do on a render pass object - command_encoder = device.create_command_encoder() - current_texture_view = present_context.get_current_texture() - render_pass = command_encoder.begin_render_pass( - color_attachments=[ - { - "view": current_texture_view, - "resolve_target": None, - "clear_value": (1, 1, 1, 1), - "load_op": wgpu.LoadOp.clear, - "store_op": wgpu.StoreOp.store, - } - ], - ) - - # Perform commands, something like ... - render_pass.set_pipeline(...) - render_pass.set_index_buffer(...) - render_pass.set_vertex_buffer(...) - render_pass.set_bind_group(...) - render_pass.draw_indexed(...) - - # When done, submit the commands to the device queue. - render_pass.end() - device.queue.submit([command_encoder.finish()]) - - # If you want to draw continuously, request a new draw right now - canvas.request_draw() - - -Starting the event loop -+++++++++++++++++++++++ - - -We can now pass the above render function to the canvas. The canvas will then -call the function whenever it (re)draws the window. And finally, we call ``run()`` to enter the mainloop. - -.. code-block:: py - - canvas.request_draw(draw_frame) - run() - - -Offscreen -+++++++++ - -If you render offscreen, or only do compute, you do not need a canvas. You also won't need a GUI toolkit, draw function or enter the event loop. -Instead, you will obtain a command encoder and submit its records to the queue directly. - - -Examples and external resources -------------------------------- - -Examples that show wgpu-py in action: - -* https://github.com/pygfx/wgpu-py/tree/main/examples - -.. note:: The examples in the main branch of the repository may not match the pip installable version. Be sure to refer to the examples from the git tag that matches the version of wgpu you have installed. - - -External resources: - -* https://webgpu.rocks/ -* https://sotrh.github.io/learn-wgpu/ -* https://rust-tutorials.github.io/learn-wgpu/ - - -A brief history of WebGPU -------------------------- - -For years, OpenGL has been the only cross-platform API to talk to the GPU. -But over time OpenGL has grown into an inconsistent and complex API ... - - *OpenGL is dying* - --- Dzmitry Malyshau at `Fosdem 2020 `_ - -In recent years, modern API's have emerged that solve many of OpenGL's -problems. You may have heard of Vulkan, Metal, and DX12. These -API's are much closer to the hardware, which makes the drivers more -consistent and reliable. Unfortunately, the huge amount of "knobs to -turn" also makes them quite hard to work with for developers. - -Therefore, higher level API are needed, which use the same concepts, but are much easier to work with. -The most notable one is the `WebGPU specification `_. This is what future devs -will be using to write GPU code for the browser. And for desktop and mobile as well. - -As the WebGPU spec is being developed, a reference implementation is -also build. It's written in Rust and powers the WebGPU implementation in Firefox. -This reference implementation, called `wgpu `__, -also exposes a C-api (via `wgpu-native `__), -so that it can be wrapped in Python. And this is precisely what wgpu-py does. - -So in short, wgpu-py is a Python wrapper of wgpu, which is an desktop -implementation of WebGPU, an API that wraps Vulkan, Metal and DX12, -which talk to the GPU hardware. - - - -Coordinate system ------------------ - -In wgpu, the Y-axis is up in normalized device coordinate (NDC): point(-1.0, -1.0) -in NDC is located at the bottom-left corner of NDC. In addition, x and -y in NDC should be between -1.0 and 1.0 inclusive, while z in NDC should -be between 0.0 and 1.0 inclusive. Vertices out of this range in NDC -will not introduce any errors, but they will be clipped. - - -Array data ----------- - -The wgpu library makes no assumptions about how you store your data. -In places where you provide data to the API, it can consume any data -that supports the buffer protocol, which includes ``bytes``, -``bytearray``, ``memoryview``, ctypes arrays, and numpy arrays. - -In places where data is returned, the API returns a ``memoryview`` -object. These objects provide a quite versatile view on ndarray data: - -.. code-block:: py - - # One could, for instance read the content of a buffer - m = device.queue.read_buffer(buffer) - # Cast it to float32 - m = m.cast("f") - # Index it - m[0] - # Show the content - print(m.tolist()) - -Chances are that you prefer Numpy. Converting the ``memoryview`` to a -numpy array (without copying the data) is easy: - -.. code-block:: py - - array = np.frombuffer(m, np.float32) - - -Debugging ---------- - -If the default wgpu-backend causes issues, or if you want to run on a -different backend for another reason, you can set the -`WGPU_BACKEND_TYPE` environment variable to "Vulkan", "Metal", "D3D12", -or "OpenGL". - -The log messages produced (by Rust) in wgpu-native are captured and -injected into Python's "wgpu" logger. One can set the log level to -"INFO" or even "DEBUG" to get detailed logging information. - -Many GPU objects can be given a string label. This label will be used -in Rust validation errors, and are also used in e.g. RenderDoc to -identify objects. Additionally, you can insert debug markers at the -render/compute pass object, which will then show up in RenderDoc. - -Eventually, wgpu-native will fully validate API input. Until then, it -may be worthwhile to enable the Vulkan validation layers. To do so, run -a debug build of wgpu-native and make sure that the Lunar Vulkan SDK -is installed. - -You can run your application via RenderDoc, which is able to capture a -frame, including all API calls, objects and the complete pipeline state, -and display all of that information within a nice UI. - -You can use ``adapter.request_device_sync()`` to provide a directory path -where a trace of all API calls will be written. This trace can then be used -to re-play your use-case elsewhere (it's cross-platform). - -Also see wgpu-core's section on debugging: -https://github.com/gfx-rs/wgpu/wiki/Debugging-wgpu-Applications - - -Freezing apps -------------- - -In wgpu a PyInstaller-hook is provided to help simplify the freezing process -(it e.g. ensures that the wgpu-native DLL is included). This hook requires -PyInstaller version 4+. - -Our hook also includes ``glfw`` when it is available, so code using ``wgpu.gui.auto`` -should Just Work. - -Note that PyInstaller needs ``wgpu`` to be installed in `site-packages` for -the hook to work (i.e. it seems not to work with a ``pip -e .`` dev install). diff --git a/docs/index.rst b/docs/index.rst index 0ca67bf..aadf641 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,10 +8,9 @@ Welcome to the rendercanvas docs! :caption: Contents: start - guide - gui api - + backends + backendapi Indices and tables diff --git a/docs/start.rst b/docs/start.rst index 21c6551..9c3975b 100644 --- a/docs/start.rst +++ b/docs/start.rst @@ -1,10 +1,10 @@ -Installation -============ +Getting started +=============== -Install with pip ----------------- +Installation +------------ -You can install ``rendercanvas`` via pip. +You can install ``rendercanvas`` via pip or similar. Python 3.9 or higher is required. Pypy is supported. .. code-block:: bash @@ -22,9 +22,41 @@ Since most users will want to render something to screen, we recommend installin Backends -------- -Multiple backends are supported, including multiple GUI libraries, see :doc:`the GUI API ` for details: +Multiple backends are supported, including multiple GUI libraries, but none of these are installed by default. See :doc:`backends` for details. + + +Creating a canvas +----------------- + +In general, it's easiest to let ``rendercanvas`` select a backend automatically: + +.. code-block:: py + + from rendercanvas.auto import RenderCanvas, loop + + canvas = RenderCanvas() + + loop.run() # Enter main-loop + + +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 + +.. code-block:: py + + present_context = canvas.get_context("wgpu") + + +Freezing apps +------------- + +In ``rendercanvas`` a PyInstaller-hook is provided to help simplify the freezing process. This hook requires +PyInstaller version 4+. Our hook includes ``glfw`` when it is available, so code using ``rendercanvas.auto`` +should Just Work. -* `glfw `_: a lightweight canvas for the desktop -* `jupyter_rfb `_: only needed if you plan on using Jupyter -* qt (PySide6, PyQt6, PySide2, PyQt5) -* wx +Note that PyInstaller needs ``rendercanvas`` to be installed in `site-packages` for +the hook to work (i.e. it seems not to work with a ``pip -e .`` dev install). diff --git a/rendercanvas/__init__.py b/rendercanvas/__init__.py index 77360fe..491276c 100644 --- a/rendercanvas/__init__.py +++ b/rendercanvas/__init__.py @@ -1,5 +1,5 @@ """ -rendercanvas: one canvas API, multiple backends +RenderCanvas: one canvas API, multiple backends. """ # ruff: noqa: F401 diff --git a/rendercanvas/_loop.py b/rendercanvas/_loop.py index 1bcbf84..102ea3a 100644 --- a/rendercanvas/_loop.py +++ b/rendercanvas/_loop.py @@ -12,7 +12,11 @@ class BaseTimer: - """Base class for a timer objects.""" + """The Base class for a timer object. + + Each backends provides its own timer subclass. The timer is used by the internal scheduling mechanics, + and is also returned by user-facing API such as ``loop.call_later()``. + """ _running_timers = set() @@ -85,12 +89,22 @@ def time_left(self): @property def is_running(self): - """Whether the timer is running.""" + """Whether the timer is running. + + A running timer means that a new call to the callback is scheduled and + will happen in ``time_left`` seconds (assuming the event loop keeps + running). + """ return self._expect_tick_at is not None @property def is_one_shot(self): - """Whether the timer is one-shot or continuous.""" + """Whether the timer is one-shot or continuous. + + A one-shot timer stops running after the currently scheduled call to the callback. + It can then be started again. A continuous timer (i.e. not one-shot) automatically + schedules new calls. + """ return self._one_shot def _init(self): @@ -120,7 +134,10 @@ def _stop(self): class BaseLoop: - """Base class for event-loop objects.""" + """The base class for an event-loop object. + + Each backends provides its own loop subclass, so that rendercanvas can run cleanly in the backend's event loop. + """ _TimerClass = None # subclases must set this diff --git a/rendercanvas/base.py b/rendercanvas/base.py index 0f2e2f4..5ccf23b 100644 --- a/rendercanvas/base.py +++ b/rendercanvas/base.py @@ -19,18 +19,24 @@ class BaseRenderCanvas: """The base canvas class. - This class provides a uniform canvas API so render systems can use - code that is portable accross multiple GUI libraries and canvas targets. + Each backends provides its own canvas subclass by implementing a predefined + set of private methods. + + This base class defines a uniform canvas API so render systems can use code + that is portable accross multiple GUI libraries and canvas targets. The + scheduling mechanics are generic, even though they run on different backend + event systems. Arguments: size (tuple): the logical size (width, height) of the canvas. - title (str): The title of the canvas. + title (str): The title of the canvas. Can use '$backend' to show the RenderCanvas class name, + and '$fps' to show the fps. update_mode (EventType): The mode for scheduling draws and events. Default 'ondemand'. min_fps (float): A minimal frames-per-second to use when the ``update_mode`` is 'ondemand'. The default is 1: even without draws requested, it still draws every second. - max_fps (float): A maximal frames-per-second to use when the ``update_mode`` is 'ondemand' or 'continuous'. - The default is 30, which is usually enough. - vsync (bool): Whether to sync the draw with the monitor update. Helps + max_fps (float): A maximal frames-per-second to use when the ``update_mode`` is 'ondemand' + or 'continuous'. The default is 30, which is usually enough. + vsync (bool): Whether to sync the draw with the monitor update. Helps against screen tearing, but can reduce fps. Default True. present_method (str | None): The method to present the rendered image. Can be set to 'screen' or 'image'. Default None (auto-select).