Expand description
A generic piet
rendering context for all windowing and graphics backends.
Windowing frameworks like winit
do not provide a way to draw into them by default. This decision
is intentional; it allows the user to choose which graphics backend that they’d like to use, and also
makes maintaining the windowing code much simpler. For games (what winit
was originally designed
for), usually a 3D rendering context like wgpu
or glow
would be used in this case. However,
GUI applications will need a 2D vector graphics context.
piet
is a 2D graphics abstraction that can be used with many different graphics backends. However,
piet
’s default implementation, piet-common
, is difficult to integrate with windowing systems.
theo
aims to bridge this gap by providing a generic piet
rendering context that easily
integrates with windowing systems.
Rather than going through drawing APIs like cairo
and DirectX, theo
directly uses GPU APIs in
order to render to the window. This allows for better performance and greater flexibility, and also
ensures that much of the rendering logic is safe. This also reduces the number of dynamic
dependencies that your final program needs to rely on.
theo
prioritizes versatility and performance. By default, theo
uses an optimized GPU backend for
rendering. If the GPU is not available, theo
will fall back to software rendering.
Usage Example
First, users must create a Display
, which represents the root display of the system. From here,
users should create Surface
s, which represent drawing areas. Finally, a Surface
can be used
to create the RenderContext
type, which is used to draw.
use piet::{RenderContext as _, kurbo::Circle};
use theo::{Display, Surface, RenderContext};
// Create a display using a display handle from your windowing framework.
// It must implement `raw_window_handle::HasRawDisplayHandle`.
let mut display = unsafe {
Display::builder()
.build(&my_display)
.expect("failed to create display")
};
// Create a surface using a window handle from your windowing framework.
// It must implement `raw_window_handle::HasRawWindowHandle`.
let surface_future = unsafe {
display.make_surface(
&window,
window.width(),
window.height()
)
};
// make_surface returns a future that needs to be polled.
let mut surface = surface_future.await.expect("failed to create surface");
// Set up drawing logic.
window.on_draw(move || async move {
// Create the render context.
let mut ctx = RenderContext::new(
&mut display,
&mut surface,
window.width(),
window.height()
).expect("failed to create render context");
// Clear the screen and draw a circle.
ctx.clear(None, piet::Color::WHITE);
ctx.fill(
&Circle::new((200.0, 200.0), 50.0),
&piet::Color::RED
);
// Finish drawing.
ctx.finish().expect("failed to finish drawing");
// Present the display.
drop(ctx);
display.present().await;
});
See the documentation for the piet
crate for more information on how to use the drawing API.
Backends
As of the time of writing, theo
supports the following backends:
wgpu
backend (enabled with thewgpu
feature), which uses thepiet-wgpu
crate to render to the window. This backend supports all of the graphics APIs thatwgpu
supports, including Vulkan, Metal, and DirectX 11/12.glow
backend (enabled with thegl
feature), which uses thepiet-glow
crate to render to the window.glutin
is used on desktop platforms to create the OpenGL context, andglow
is used to interact with the OpenGL API. This backend supports OpenGL 3.2 and above.- A software rasterization backend.
tiny-skia
is used to render to a bitmap, and thensoftbuffer
is used to copy the bitmap to the window. This backend is enabled by default and is used when no other backend is available.
Performance
As theo
implements most of its own rendering logic, this can lead to serious performance
degradations if used improperly, especially on the software rasterization backend. In some cases,
compiling theo
on Debug Mode rather than Release Mode can half the frame rate of the application.
If you are experiencing low frame rates with theo
, make sure that you are compiling it on Release
Mode.
In addition, gradient brushes are optimized in such a way that the actual gradient needs to be computed only once. However, this means that, if you re-instantiate the brush every time, the gradient will be re-computed every time. This can lead to serious performance degradations even on hardware-accelerated backends. The solution is to cache the brushes that you use. For instance, instead of doing this:
let gradient = /* ... */;
window.on_draw(|| {
let mut ctx = /* ... */;
ctx.fill(&Circle::new((200.0, 200.0), 50.0), &gradient);
})
Do this, making sure to cache the gradient brush:
let gradient = /* ... */;
let mut gradient_brush = None;
window.on_draw(|| {
let mut ctx = /* ... */;
let gradient_brush = gradient_brush.get_or_insert_with(|| {
ctx.gradient_brush(gradient.clone()).unwrap()
});
ctx.fill(&Circle::new((200.0, 200.0), 50.0), gradient_brush);
})
theo
explicitly opts into a thread-unsafe model. Not only is thread-unsafe code more performant,
but these API types are usually thread-unsafe anyways.
Structs
- The brushes used to draw to a
Surface
. - The display used to manage all surfaces.
- A builder containing system-specific information to create a
Display
. - The images used to draw to a
Surface
. - The context used to draw to a
Surface
. - The surface used to draw to.
- The text backend for the system.
- The text layout for the system.
- The text layout builder for the system.
Type Definitions
- An error handler for GLX.