A WebGPU
Graphics Device
for R
@yutannihilat_en
R’s standard library provides graphics-related functionalities:
High-level functions (e.g. plot()
) that draw nice plot automagically.
Low level functions that work behind high-level ones (e.g. grid.lines()
); they does very primitive operations like drawing a line or a rectangle.
Graphics device is the layer that actually executes those low-level operations.
The operations are translated via Graphics Device API
We can create a graphics device by implementing the Graphics Device API. For example…
A device that drives pen-plotter
A device that translate the drawing operations into sounds
A device that ignores all the operations (null device)
circle() |
Draw a circle |
rect() |
Draw a rectangle |
line() |
Draw a line |
text() , textUTF8() |
Draw a text |
metricInfo() |
Return the width and height of the text |
clip() |
Set the clipping range |
activate() , deactivate() , close() |
Hooks that are called when a device is opened or closed. |
WebGPU exposes an API for performing operations, such as rendering and computation, on a Graphics Processing Unit. (ref: WebGPU spec)
As the word “Web” indicates, it’s designed for web browsers.
But, it doesn’t mean it’s only for the Web1
There already exist several graphics APIs that utilize GPU. However, different APIs are required for different platforms / OSes.
OS | Graphics API |
---|---|
Windows | Direct3D 12 (or 11), Vulkan |
macOS | Metal (, MoltenVK) |
Linux | Vulkan |
So, we need another abstraction layer over them.
(This point is less important in the context of implementing R’s graphics device)
Considering the usage on web browsers, the API should be safe. The API should protect users from problems like malicious attacks and unexpected crashes.
In terms of this, the native APIs are too raw.
-GL APIs are great in portability (actually WebGPU implementations use OpenGL ES as one of the backends)
However, -GL APIs’ design is not in line with modern GPU architectures, which causes computational and mental overhead 1
Learn Wgpu: https://sotrh.github.io/learn-wgpu/
Why not! I want a graphics device that can be messed up with shader magics (e.g. post effects)
One serious reason is that there’s no an interactive graphics device that’s available on all of macOS, Linux, and Windows1
How can R call the Rust-implemented graphics device?
How can the Rust-implementation access data contained by R?
R has the C API
Rust can use FFI
→ Generate a Rust bindings for R’s C API by rust-bindgen, and wrap it nicely
“A safe and user friendly R extension interface using Rust”
Developed since 2020
(Although I know almost nothing about Rust, I’m one of the maintainers…)
(Not implement yet)
In my implementation, texts are rendered with tessallation, but it can be with rasterization.
API | Implementation | Reason |
---|---|---|
line() |
tessellation | |
circle() |
SDF | A circle is drawn repeatedly (e.g. scatterplot), so SDF should be more efficient |
rect() |
tessellation | |
text() |
tessellation | SDF font might be more efficient, but I don’t know how to implement it… |
raster() |
(not implemented yet) | |
… |
It works!
Invert colors
Retro CRT monitor effect (based on a blog post by Babylon.js)
let CURVATURE: vec2<f32> = vec2<f32>(3.0, 3.0);
let RESOLUTION: vec2<f32> = vec2<f32>(100.0, 100.0);
let BRIGHTNESS: f32 = 4.0;
let PI: f32 = 3.14159;
fn curveRemapUV(uv_in: vec2<f32>) -> vec2<f32> {
var uv_out: vec2<f32>;
// as we near the edge of our screen apply greater distortion using a cubic function
uv_out = uv_in * 2.0 - 1.0;
var offset: vec2<f32> = abs(uv_out.yx) / CURVATURE;
uv_out = uv_out + uv_out * offset * offset;
return uv_out * 0.5 + 0.5;
}
fn scanLineIntensity(uv_in: f32, resolution: f32, opacity: f32) -> vec4<f32> {
var intensity: f32 = sin(uv_in * resolution * PI * 2.0);
intensity = ((0.5 * intensity) + 0.5) * 0.9 + 0.1;
return vec4<f32>(vec3<f32>(pow(intensity, opacity)), 1.0);
}
fn vignetteIntensity(uv_in: vec2<f32>, resolution: vec2<f32>, opacity: f32, roundness: f32) -> vec4<f32> {
var intensity: f32 = uv_in.x * uv_in.y * (1.0 - uv_in.x) * (1.0 - uv_in.y);
return vec4<f32>(vec3<f32>(clamp(pow((resolution.x / roundness) * intensity, opacity), 0.0, 1.0)), 1.0);
}
@vertex
...snip...
@fragment
fn fs_main(
vs_out: VertexOutput
) -> @location(0) vec4<f32> {
var remapped_tex_coords = curveRemapUV(vs_out.tex_coords);
var color: vec4<f32> = textureSample(r_texture, r_sampler, remapped_tex_coords);
color *= vignetteIntensity(remapped_tex_coords, RESOLUTION, 1.0, 2.0);
color *= scanLineIntensity(remapped_tex_coords.x, RESOLUTION.y, 1.0);
color *= scanLineIntensity(remapped_tex_coords.y, RESOLUTION.x, 1.0);
return vec4<f32>(color.rgb * BRIGHTNESS, 1.0);
}
If Rust panics, the R session crashes immediately so it’s hard to debug. I need to decouple the implementation from R-related codes.
Interface to accept shader code from users. Currently I have no idea how to express the operations that needs to be applied repeatedly (e.g. bloom effect)
Learn Wgpu: https://sotrh.github.io/learn-wgpu/
extendr: https://extendr.github.io/