Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 8074bd0

Browse files
committed
[Impeller] Document how to use Impeller as a standalone library with OpenGL ES and EGL.
1 parent 55da126 commit 8074bd0

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

impeller/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,4 @@ To your `Info.plist` file, add under the top-level `<dict>` tag:
241241
* [Important Benchmarks](docs/benchmarks.md)
242242
* [Threading in the Vulkan Backend](docs/vulkan_threading.md)
243243
* [Android Rendering Backend Selection](docs/android.md)
244+
* [Using Impeller as a Standalone Rendering Library (with OpenGL ES)](docs/standalone_gles.md)

impeller/docs/standalone_gles.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Using Impeller as a Standalone Rendering Library (with OpenGL ES)
2+
3+
This guide describes how to use Impeller as a standalone rendering library using OpenGL ES. Additionally, some form of WSI (Window System Integration) is essential. Since EGL is the most the most popular form of performing WSI on platforms with OpenGL ES, Impeller has a toolkit that assists in working with EGL. This guide will use that toolkit.
4+
5+
While this guide focuses on OpenGL ES with EGL, the steps to setup rendering with another client rendering API (Metal and Vulkan) are fairly similar and you should be able to follow the same pattern for other backends.
6+
7+
This guide details extremely low-level setup and the `//impeller/renderer` API directly above the HAL. Most users of Impeller will likely use the API using convenience wrappers already written for the platform. Interacting directly with the HAL is extremely powerful but also verbose. Applications are likely to also use higher level frameworks like Aiks or Display Lists.
8+
9+
Building Impeller for the target platform is outside the scope of this guide.
10+
11+
> [!CAUTION]
12+
> The code provided inline is pseudo-code and doesn't include error handling. See the headerdocs for more on error handling and failure modes. All classes are assumed to be in the `impeller` namespace. For a more complete example of setting up standalone Impeller, see [this patch](https://github.com/flutter/engine/pull/52472/files) that adds support for Impeller rendering via Wasm [in the browser and WebGL 2](https://public.chinmaygarde.com/impeller/wasm/wasm.html).
13+
14+
# Setup
15+
16+
To get started with Impeller rendering, you need to setup a context and a renderer. For backend specific classes, the convention in Impeller is to append the backend name as a suffix. So if you need to create an instance of an `impeller::Context` for OpenGLES, look for `impeller::ContextGLES`.
17+
18+
In our case, we need a `impeller::ContextGLES` and `impeller::Renderer`.
19+
20+
## Setting up WSI with the EGL Toolkit
21+
22+
Before we get to `OpenGLES` we need to do a bit of WSI. While we can resort to using EGL directly, Impeller has a handy toolkit for it.
23+
24+
First create an EGL display connection:
25+
26+
```c++
27+
egl::Display display;
28+
```
29+
30+
Ask the display for a valid EGL configuration. Impeller needs and OpenGL ES 2.0 configuration.
31+
32+
```c++
33+
egl::ConfigDescriptor egl_desc;
34+
egl_desc.api = egl::API::kOpenGLES2;
35+
egl_desc.samples = egl::Samples::kOne;
36+
egl_desc.color_format = egl::ColorFormat::kRGBA8888;
37+
egl_desc.stencil_bits = egl::StencilBits::kZero;
38+
egl_desc.depth_bits = egl::DepthBits::kZero;
39+
egl_desc.surface_type = egl::SurfaceType::kWindow;
40+
41+
auto config = display.ChooseConfig(egl_desc);
42+
```
43+
44+
Once a valid config has been obtained, create a context and window surface. Creating the window surface requires a native window handle. Get the appropriate one for your platform. For instance, on Android, this is an `ANativeWindow`.
45+
46+
```c++
47+
auto context = display.CreateContext(*config, nullptr /* sharegroup */ );
48+
auto surface = display.CreateWindowSurface(*config, native_window_handle);
49+
```
50+
51+
Now that we have context, make it current on the calling thread. This should be fully setup WSI.
52+
53+
```c++
54+
context->MakeCurrent(*surface);
55+
```
56+
57+
## Creating the OpenGL ES Context
58+
59+
Impeller doesn't statically link against OpenGL ES. You need to give it a callback the return the appropriate OpenGL ES function for given name. With EGL, this can be something as simple as:
60+
61+
```c++
62+
auto resolver = [](const char* name) -> void* {
63+
return reinterpret_cast<void*>(::eglGetProcAddress(name));
64+
};
65+
```
66+
67+
Adjust as necessary.
68+
69+
Once you have the resolver, you need to create an OpenGL ES proc table. The proc table contains the table of OpenGL ES procs that Impeller will need at runtime.
70+
71+
```c++
72+
auto gl = std::make_unique<ProcTableGLES>(resolver);
73+
```
74+
75+
Once the proc table is created, the resolver will no longer be invoked.
76+
77+
Then you need to provide the context a shader library that contains a manifest of all the shaders the context will need at runtime. Remember, Impeller doesn't generate shaders at runtime. Instead `impellerc` generates blobs that can either be delivered out of band or be embedded directly in the binary. When embedding the blobs directly in the binary, find the symbols referring to the shader blob somewhere in the generated build artifacts. A vector of mappings to these blobs needs to be provided to create context creation factory.
78+
79+
An example of creating an embedded mapping is provided below. Adjust as necessary depending on how to plan on delivering shader blobs to Impeller at during setup.
80+
```c++
81+
82+
#include "impeller/fixtures/gles/fixtures_shaders_gles.h" // <---- Depends on your application.
83+
84+
static std::vector<std::shared_ptr<fml::Mapping>>
85+
ShaderLibraryMappingsForApplication() {
86+
return {
87+
std::make_shared<fml::NonOwnedMapping>(
88+
impeller_fixtures_shaders_gles_data,
89+
impeller_fixtures_shaders_gles_length),
90+
};
91+
}
92+
93+
// In the setup routine.
94+
95+
auto mappings = ShaderLibraryMappingsForApplication();
96+
```
97+
98+
And that's it. You have all the ingredients necessary to create a context. Create one now.
99+
100+
```c++
101+
auto context = ContextGLES::Create(
102+
std::move(gl), // proc table
103+
std::move(mappings), // shader libraries
104+
false // enable tracing
105+
);
106+
```
107+
108+
Now for a tricky bit about OpenGL ES. Impeller is multi-threaded. Even when using OpenGL ES, objects above the Impeller HAL can be created, used, and consumed on any thread. But OpenGL ES isn't. Impeller also doesn't know anything about EGL. So it is your responsibility to tell Impeller which threads are safe to use OpenGL ES on.
109+
110+
In our little toy setup, we only have a single thread. We tell Impeller which thread to use by creating a subclass of an `impeller::ReactorGLES::Worker`. Let's create a simple worker:
111+
112+
```c++
113+
class ReactorWorker final : public impeller::ReactorGLES::Worker {
114+
public:
115+
ReactorWorker() = default;
116+
117+
// |ReactorGLES::Worker|
118+
~ReactorWorker() override = default;
119+
120+
ReactorWorker(const ReactorWorker&) = delete;
121+
122+
ReactorWorker& operator=(const ReactorWorker&) = delete;
123+
124+
// |ReactorGLES::Worker|
125+
bool CanReactorReactOnCurrentThreadNow(
126+
const ReactorGLES& reactor) const override {
127+
return true;
128+
}
129+
};
130+
```
131+
132+
Add an instance of this reactor worker to the context. Whew, that was unnecessarily complicated. But this is only necessary for OpenGL ES because of its tricky threading. Skip this step for Vulkan and Metal. Those APIs are already thread safe.
133+
134+
```c++
135+
context->AddReactorWorker(worker);
136+
```
137+
138+
Once you have the context, use it to crate a renderer.
139+
140+
```c++
141+
auto renderer = std::make_shared<Renderer>(context);
142+
```
143+
144+
And setup is done. Keep the context and renderer around. We will be using it during frame rendering.
145+
146+
# Rendering Frames
147+
148+
Rendering frames is a matter of:
149+
* Wrapping the onscreen texture as a Surface (`impeller::SurfaceGLES` in our case).
150+
* Giving the surface to our renderer to render.
151+
* The renderer will prepare a render target from this surface and invoke a callback that we'll use to setup a render pass directed at that render target.
152+
* Populate the render pass how we see fit.
153+
* Tell our reactor worker that all operations need to be flushed to OpenGL.
154+
* Present the onscreen surface.
155+
* Repeat.
156+
157+
## Wrap the Onscreen Surface
158+
159+
Per frame, the onscreen surface can be wrapped using SurfaceGLES::WrapFBO where the default framebuffer in our case if FBO 0. Take care to to ensure that the pixel format matches the one we used to choose the EGL config. Figuring out the pixel size is left as an exercise for the reader.
160+
161+
```c++
162+
auto surface =
163+
SurfaceGLES::WrapFBO(context, // context
164+
swap_callback, // swap callback
165+
0u, // fbo
166+
PixelFormat::kR8G8B8A8UNormInt, // pixel format
167+
GetWindowSize() // surface size
168+
);
169+
```
170+
171+
## Setup the Swap Callback
172+
173+
The swap callback will get invoked when the renderer presents the surface. Remember in our list of things to do, we need to first tell the reactor worker to flush all pending OpenGL operations and then present the surface. Setup the swap callback appropriately.
174+
175+
```c++
176+
SurfaceGLES::SwapCallback swap_callback =
177+
[surface, context]() -> bool {
178+
context->GetReactor()->React();
179+
return surface->Present();
180+
};
181+
```
182+
183+
## Render to the Surface
184+
185+
Give the surface to the rendere along with a callback that details how you will populate the render target the renderer sets up that is directed at that surface.
186+
187+
```c++
188+
renderer_->Render(std::move(surface),
189+
[&](RenderTarget& render_target) -> bool {
190+
// Do things that render into the render target.
191+
return true;
192+
});
193+
```
194+
195+
And that's it. Now you have functional WSI and render loop. Higher level frameworks like Aiks and DisplayList that use the render target to render their rendering into to the surface.

0 commit comments

Comments
 (0)