GSoC 2026: GPU-Accelerated Brush Engine and integration with Drawing Tablet Input #4195
Replies: 10 comments
Week 1This weeks coding was focused on finishing stuff that was originally planned to be done before GSoC. Namely the introduction of Resources, a way to store binary data addressable by content hash alongside the node graph. For context #4148 (was merged before GSoC). The resource system was considered a requirement for my GSoC project because without it auto saves stall the editor every second (when raster content is involved) making any kind of drawing nearly impossible. The following video illustrates that, starting with a graphite version right before the introduction of resources and the second version with my patches applied. untitled.mp4A nice side effect is the reduction in file size (before images where stored as raw pixel data inside the graph). File size reduction for the document in the video from 202.6MB to 18.9MB. #4165 also embeds fonts as resources allowing documents to render without fetching the fonts from the internet first. The rest of my time was spend researching for the brush engine and stroke data format. Looking at things like "Ciallo: GPU-Accelerated Rendering of Vector Brush Strokes". |
Week 2This week was spend experimenting with different brush rendering techniques in graphite. 2026-06-21.17-22-59.mp4With a very simple WGPU Pipeline backed brush I was already able to get lower latency that the CPU implementation. 2026-06-22.17-41-31.mp4There was also continued work on systems that are only indirectly related. #4194 #4201 #4225 |
Week 3This week was focused on preparing infrastructure that will make it easier/cleaner to implement a lot of GPU Pipelines. In merge order, all part of the same goal, 3 approaches where tried, final results:
Combined this allows Pipeline definition to be greatly simplified and locally (before all pipelines lived in the somewhat monolithic Defining a pipeline and using it is now as simple as: #[node_macro::node(...))]
pub async fn airbrush<'a: 'n>(
_ctx: impl Ctx,
#[scope(airbrush_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache,
...,
) -> List<Raster<GPU>> {
let args = AirbrushPipelineArgs { ... };
pipeline.run::<AirbrushPipeline>(&args).await
}
#[node_macro::node(category(""), inject_scope)]
async fn airbrush_pipeline<'a: 'n>(_ctx: impl Ctx, #[scope(WGPU_EXECUTOR_IDENTIFIER)] executor: &'a WgpuExecutor, #[data] pipeline: WgpuPipelineCache) -> WgpuPipelineCache {
executor.pipeline_init::<AirbrushPipeline>(pipeline);
pipeline.clone()
}
pub struct AirbrushPipeline { ... }
pub struct AirbrushPipelineArgs<'a> { ... }
impl AsyncWgpuPipeline for AirbrushPipeline {
async fn create(...) { ... }
async fn run(...) { ... }
} |
Week 4In this week I worked on implementing more POC rendering technique implementations for brushes. 2026-06-23.10-57-58.mp42026-06-23.11-41-14.mp4
I also worked on improving build tooling that I was annoyed about for some time #4254 #4256 #4266 . |
2026-06-23.14-17-57.mp4 |
2026-07-19.23-00-33.mp42026-07-19.22-58-03.mp4 |
2026-07-19.23-46-46.mp4 |
2026-08-20.10-35-05.mp4 |




Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Synopsis
Graphite currently has no support for drawing tablets or touch input, and its existing brush tool relies on a slow, CPU-based implementation with no awareness of resolution or zoom level. This project introduces drawing tablet and touch device support as first-class input sources, then builds a high-performance, GPU-accelerated brush engine on top, storing strokes in a resolution-independent format inspired by Graphite's non-destructive philosophy and rendering them to texture via WGSL shaders or another approach if research points to a better path. A final phase connects the two by exposing stylus-specific signals such as pressure and tilt as brush parameters, allowing artists to take full advantage of the engine's capabilities through their hardware.
Deliverables
GSoC 2026 Final Report: GPU-Accelerated Brush and Drawing Tablet Input
2026-08-23.18-02-01.mp4
The video above shows the airbrush being used with a simple drawing tablet, and how a stroke changes with the brush parameters. It shows both parts of this project in action. Raw user input is recorded in Graphite's node graph and then rendered for the current viewport using a GPU pipeline. All parts involved are optimized for low input latency, which is very important when using interactive drawing tools.
One can test the web version here (Brush needs to be enabled in preferences):
https://simple-gpu-airbrush-pr.graphite.pages.dev (updated untill the final PR is merged)
https://77706ab3.graphite.pages.dev (pinned to state on the 2026-08-23)
Desktop builds can be found in the final PR #4354 (needs GitHub login to download)
2026-08-23.18-05-55.mp4
The document stores strokes, not pixels, so the airbrush renders them again whenever the view changes. The video above zooms into a stroke and switches between the normal render mode and pixel preview mode, which shows how the drawing would look exported at 1x, or drawn in a pixel based tool. The stroke keeps its detail at every zoom level, only the preview is pixelated.
Graphite already had a brush tool that renders on the CPU. The video below shows how slow it is to draw with.
2026-08-23.18-05-14.mp4
And the same kind of drawing with the GPU airbrush.
2026-08-23.18-03-54.mp4
Drawing tablet and pen input
When my GSoC Project started, Graphite treated every pointing device as a mouse. A pen on a drawing tablet moved the cursor, but pointer down, pressure, tilt, and all other stylus signals were thrown away before they reached the editor. The original plan assumed I would first have to contribute tablet event support to winit, the windowing library used by Graphite's desktop app. That turned out to be unnecessary, winit had drawing tablet support upstream by the time the coding period began. The work shifted to integrating it.
#4354 brings tablet input to the desktop app. The interesting part is not reading the pen data, it is where the data goes. Graphite's desktop UI is rendered by an embedded browser (CEF), and input used to travel through it. The window forwarded events to CEF, the web UI turned them into DOM events, and the editor received whatever survived that round trip. Pen input now takes a direct route (for reduced latency). Getting this right involved a pile of platform edge cases, from pointer lock interactions to Windows resize helper fixes that silently swallowed pen input.
#4445 is the web counterpart. The browser already delivers pen data through the Pointer Events API, but Graphite used to ignore most of it. The frontend now captures the pointer during a stroke and reads pressure, tilt, twist, tangential pressure, and the eraser state for pen pointers. The browser normally delivers only one pointer event per frame, so a fast stroke would lose most of its samples. Reading coalesced events fixes this. Each event also carries the samples that arrived since the previous one.
#4443 adds a timestamp to every pointer input event. Brushes that change over time, like an airbrush that keeps depositing paint while hovering, need to know when each input sample happened, not just where. The stroke format described in the next section stores this as a per-sample time channel. The airbrush built during GSoC does not use it yet, but future brushes will.
A non-destructive stroke format
#4467 introduces the stroke data format, the core data model of the project. A stroke is the recorded pen input: a list of positions plus channels for pressure, tilt, twist, and time. A channel stores either one value per sample or a single uniform value, so a mouse stroke does not pay for per-sample data it does not have. Each stroke also carries a seed, so brushes that use randomness render the same way every time.
What exactly to store took multiple rounds of discussion. The core question: does the document store the raw input the artist produced, or the mapped values a brush actually uses, for example pressure already converted into a diameter? Mapped values would make rendering simpler and keep old strokes stable when a brush changes. But they bake decisions into the data: once pressure has become diameter, you cannot go back and map it to opacity instead. We settled on raw input, mainly because it follows Graphite's non-destructive philosophy. The document stores what the artist did, every mapping decision stays reversible, and rendering to pixels happens later.
Storing raw input is also what makes the format brush-independent. The same stroke can be fed to the airbrush today and to any future brush, because no brush-specific interpretation is baked in. Future work will add nodes that operate on this raw input directly: mapping any pen axis to any brush parameter (tilt to size, pressure to flow), or editing strokes procedurally like any other data in the graph. We also considered desinging a intermidiate format that is purely attribute based and allows for brush parameters to be freely adjusted per sample, but in the end had to skip that due to time running out.
Rendering strokes on the GPU
#4469 adds the first GPU brush node. It currently takes strokes as input, renders them with a few wgsl shader passes into a texture sized to the intersection of footprint and the strokes bounding box, and returns that texture as raster. This is the PR everything else has been building towards.
The usual way to draw a stroke is to stamp a brush texture along it at small intervals. The airbrush works differently. A "real" (in a ignoring friction kinda way) airbrush sprays paint continuously while it moves, so every pixel asks how much paint the brush deposited while it swept past (currently ignoring time).
The brush shape is a super-Gaussian, with$v$ across and $s$ along the stroke:
Sweeping that shape along a segment has no closed form for most exponents, so I precompute it numerically into a lookup texture. Row$v$ stores the sweep integrated up to $t$ , scaled so that the middle of a long stroke reaches a density of exactly 1:
The paint a segment of length$l$ leaves at a pixel is $F(v, t) - F(v, t - l)$ , so a pixel costs two texture samples per segment, no stepping along the stroke.
Rendering a stroke runs two passes. The scatter pass draws each segment as one quad, covering the segment plus the distance at which the kernel vanishes, and adds the paint it deposits into a density texture. A second texture collects the strongest single dab covering each pixel. The resolve pass takes the larger of the two, so a stroke that barely moved still paints a full dab, converts that to opacity, and blends it with the stroke color. (A third pass converts the result into the output texture format the rest of the graph expects, and will be removed at some point)
Pressure scales the width of the kernel per input sample, so a light touch paints a thin line. Density$d$ becomes opacity through a saturating curve with a fixed gain $g$ :
Painting over the same spot again approaches full color instead of clipping at it. Flow scales the density a sample deposits, mapped through the inverse of that curve, so a flow of 100% reaches full color in a single pass.
Making the diameter mean the same thing at every hardness took surprisingly long to get right. A soft kernel and a hard kernel of the same nominal width paint visibly different line widths, because the soft one fades out well before its nominal edge. So after baking a kernel I measure it: I look for the distance at which a long stroke drops to 5% opacity, and scale the kernel so that contour lands on half the set diameter.
Baking is not free, so kernels are cached, keyed by the rounded exponent. Their textures are held weakly, and are baked again if the texture cache evicted them.
Rendering a stroke once is relatively easy. Rendering it while it is being drawn is the hard part. Every new input sample extends the stroke, and re-rendering the whole stroke history each frame would get slower the longer you draw.
#4468 adds the brush cache that makes rendering incremental. It is a node input like any other value. The cache stores one opaque value per footprint (the transform and resolution a node renders at), and the brush node decides what that value is:
The cache does not know what is in the state, its only responsibility is deciding how long to keep it. Panning or zooming produces a new footprint every frame, so a cache keyed by footprint alone would grow without limit. The cache therefore groups footprints by view (the transform without the panning) and keeps at most three of them alive, which covers the viewport and thumbnails (this may be adjusted later). Slots that have not been asked for in a few frames are retired.
The airbrush keeps the work of previous frames in that value. Finished strokes are rendered once into a cached image, and a frame reuses that image as long as the strokes in it are unchanged, rendering only what was added. The stroke being drawn keeps its density field and the number of samples already scattered into it, so a frame only scatters the samples that arrived since the last one; appending to a stroke reuses the field, editing it starts over. The segment from the last sample to the current pen position is drawn into a scratch copy instead, so the line follows the pen without committing anything the next sample would have to undo.
The finished output texture is cached too, keyed by the hashes of all strokes, so a frame in which nothing changed at all costs one lookup. Every texture in the state is held weakly, so the texture cache can reclaim it and the next frame re-renders what got freed.
Infrastructure built along the way
Brushes touch many parts of Graphite, and several things they needed did not exist yet in the way I wanted for my brush implementations. This section covers the infrastructure I built for this project: letting nodes bring their own GPU pipelines, keeping GPU memory under control, and some smaller fixes. A lot of work in this section was not anticipated beforehand.
Custom GPU pipelines for nodes
The airbrush needs its own shaders, and Graphite had no way to give a node any. All GPU pipelines lived hardcoded inside the central
WgpuExecutor, so adding GPU functionality meant editing that one crate. #4210 turns pipelines into nodes: a pipeline node owns a pipeline and provides it to other nodes through the scope system. A pipeline is a type with a small trait, created lazily on first use and cached after that. The airbrush has its own pipeline node and receives its pipelines through a scope input.For that to work, pipeline nodes have to be available everywhere without the user wiring them up. #4221 adds
inject_scopesupport to the node macro: a node can now declare that it should be injected into the network's global scope (done in the preprocessor), which makes its output available as a scope input to any other node.The rest of the stream is rough edges found while using this at scale. #4235 moves scope resolution in the graph compiler to before network flattening, fixing resolution problems the pipeline nodes ran into. #4284 hides scope inputs in the Properties panel, since they are plumbing and not values a user should see or edit. #4375 removes a node macro special case the scope work made obsolete.
Keeping GPU memory under control
Brushes are texture-hungry. Every frame touches density fields, baked kernel tables, cached stroke images, and output textures, and all of them live in GPU memory. Making that reliable turned into its own stream of work. The first problem was a crash. #4333 fixes a wgpu validation error where a cached texture was destroyed while newly queued GPU work still referenced it.
The second problem is that a cache that only grows is just a slow leak. #4447 adds weak references to the texture cache. When a node holds a texture weakly, the cache may evict it under memory pressure, and the node re-renders it on demand. The brushes use this for everything they cache, trading re-render time for bounded GPU memory. The same PR adds support for more texture formats, which the airbrush needs for its single-channel float density fields and kernel tables.
The last problem only shows on the web. Browsers free wgpu textures through the JS garbage collector, which does not currently feel GPU memory pressure on at least firefox and chromium, so textures are freed far too late and GPU memory fills up quickly. Graphite destroys its own textures manually, but the textures vello (Graphite's renderer) creates internally were out of reach. I upstreamed vello#1777, which makes vello destroy its image textures explicitly instead of waiting for garbage collection. #4446 pulls that fix into Graphite and makes the blob IDs passed to vello stable across frames, so vello can reuse its texture atlas slots instead of re-uploading every frame.
Smaller fixes
The render footprint (the resolution and transform a node renders at) was passed down in logical pixels, so on hiDPI displays nodes rendered textures at the wrong size. #4280 changes footprints to always be in physical pixels, with explicit conversion where logical space is really wanted. Brushes render strokes into textures sized by the footprint, so this was needed to make strokes crisp at any display scale.
On Linux using WGPU's OpenGL backend did not work correctly with brush shaders. Textures came back empty, underlying cause still needs investigation. #4270 makes the desktop app prefer Vulkan instead.
Side quests (Not counted towards GSoC hours)
Not everything I did these past three months was strictly part of the project. This and other work not mentioned here I did not count towards the time intended for GSoC. In most cases it was done while waiting for things that blocked project work. Some of it was work I had started before GSoC and wanted to land properly, but most of it was problems I stumbled into while iterating on the project and could not leave alone. This section covers the larger chunks (the rest is mentioned as PRs further down).
A more solid desktop app
I did almost all tablet and brush iteration in the desktop app, so I was basically stress testing the desktop app as a side effect. The desktop UI is rendered by CEF (embedded Chromium), and running CEF inside the main process caused increasing crashyness plus drawing tablet input issues on macOS I never got to the bottom of. I got tired of dealing with it and spend some time on the following. #4321 isolates CEF into its own crate and process, controlled over IPC, with frames returned through shared memory or shared GPU textures. #4322 was a quick followup to fix some shortcuts on macOS.
Two more fixes came straight out of brush iteration. #4317 fixes a crash where GPU work was submitted while the window surface was being reconfigured; with a brush node in the document, resizing the window was likely to crash Graphite. #4348 stops compiling the editor into the wasm wrapper: the desktop build used to compile the whole editor to WASM alongside the native editor, and dropping that cut desktop build times and made iterating way more comfortable (#4353 fixes a regression this caused).
While testing whether the newest winit beta fixed a niche drawing tablet issue, I stumbled onto something else: copy and paste in Graphite stopped working on Wayland. The new winit version gained a drag and drop API (we are also looking forward to that in Graphite), and to implement it, winit now creates a Wayland data device for the seat. External clipboard crates, like the one Graphite uses, create their own data device for the same seat, and the two conflict. The Wayland spec is vague on whether a seat may have more than one data device, compositors handle it differently, and at least KWin now tracks it as a bug. The clean way out is for winit to provide the clipboard itself, which was planned but not implemented, so I stepped in: winit#4658 adds a clipboard API on top of winit's new data transfer model, implemented on Wayland, macOS, and Windows, and tested against Graphite's desktop app. Once merged, this lets us upgrade winit, drop the other clipboard crate and take advantage of its improved drawing tablet and touch support. As of writing this the only thing that I think is missing is the X11 implementation.
Faster iteration with custom dev tooling
Graphite's dev server used to depend on
wasm-pack,cargo-watch, andconcurrently. #4254 replaces all three with the in-repo cargo-run tool, which calls the underlying build steps directly, leading to fewer external dependencies, readable commands, and more control over the build. #4256 makes the tool auto-install itswasm-optdependency, and #4266 moves branding and package install into it too. Made iterating on the project code a lot faster.Embedded resources in documents
Graphite documents stored raster content as raw pixel data inside the node graph. Every autosave serialized all of it, which stalled the editor every second once any raster content was involved. You cannot draw into a document that freezes every second, so fixing this was good for the project. The fix was resources: binary data stored alongside the node graph and addressed by its content hash. I started that even before the community bonding period with #4148, and finishing it properly was part of my first week of coding.
untitled.mp4
The video shows the same document before and after: autosave stalls while drawing, then smooth drawing with resources in place. A nice side effect is file size; the document in the video shrank from 202.6MB to 18.9MB.
#4168 and #4176 introduce
ResourceIdand switch resource references over to it. #4187 makes a resource carry its own content hash, so consumers do not re-hash large blobs. #4165 embeds fonts as resources, letting documents render without first fetching fonts from the internet. #4296 makes copy and paste between documents work. It now carries embedded resources along instead of leaving dangling references behind.Results and reflections
The project produced a good final result; one can plug in a drawing tablet and draw pressure-sensitive strokes into a Graphite document, on desktop and on web. Strokes are stored non-destructively, and the airbrush renders them on the GPU fast enough for live drawing and fully resolution independent.
During the coding period I opened 42 PRs, plus 2 upstream in vello and winit.
Not everything went as planned. The upstream winit tablet work from the proposal turned out to be unnecessary. Brush rendering took multiple prototype rounds before I decided (with mentor approval) to focus on implementing one brush rendering technique that demonstrates the capabilities of the infrastructure I've built and achieves the most important things the project strived for. The end result is a brush that is fun to use and useful for a wide range of artists.
The real time sinks were the unanticipated infrastructure work (most of it useful to Graphite beyond brushes) and the early, over-ambitious testing of all kinds of brush rendering techniques. I learned a lot researching brush rendering, but with the advantage of hindsight I would get one very simple brush working and polished first, and push the more advanced rendering further back.
Acknowledgements
Thanks to @TrueDoctor for being a great mentor and especially for the patience when discussing the preprocessor changes I needed. Thanks to @Keavon for being a great mentor as well and especially for the helpful data format design discussions. Thanks to @0HyperCube for finding a lot of bugs in my brush tool implementation. And thanks to my friends (that are way better at drawing than me) for testing and criticizing my work. Seeing you draw with what I've built gave me joy and motivation.
Future directions
List of PRs
Core of the project
#4354: Desktop: Support input from drawing tablets (open, 2026-07-19, stack #4444 base)
Routes drawing tablet events from winit into Graphite's input pipeline on the desktop app, making pen position, pressure, and related axes available to the editor as high-fidelity pointer input. This is the desktop half of the project's input deliverable; Capture pen input via web api #4445 covers the web half.
#4443: Add time to pointer inputs (open, 2026-08-17, stack #4444)
Attaches a timestamp to every pointer input event. Brushes that change over time need to know when each input sample happened, not just where. The stroke format introduced in Brush stroke types #4467 stores this as a per-sample time channel. The brush implemented during GSoC ended up not using the time channel yet, but future brushes will (it may also be added to the airbrush at some point).
#4445: Capture pen input via web api (open, 2026-08-17, stack #4444)
Captures pen input in the browser through the Pointer Events web API, allowing pressure, tilt, and other axes to reach the editor in the web version of Graphite. Keeps the web build at feature parity with the desktop tablet support from
Desktop: Support input from drawing tablets #4354 as far as the platform allows.
#4467: Brush stroke types (open, 2026-08-21, stack #4444)
Introduces the stroke data format, the project's core data model. A stroke is a series of input samples; each channel (pressure, tilt, twist, time etc.) is stored either per sample or as a single uniform value. The document stores what the artist did, and rendering to pixels happens later, at whatever resolution the view needs, which keeps strokes resolution independent. This implements the non-destructive stroke format deliverable.
#4468: Brush cache (open, 2026-08-21, stack #4444)
Adds a render state cache for brush nodes, keyed by footprint (the view's transform and resolution). While drawing, the renderer (brush node) reuses the state from the previous frame and only processes what changed, instead of re-rendering the whole stroke history every frame. The cache keeps a bounded number of views alive (viewport and thumbnails) and retires stale entries, so continuous zooming or panning cannot grow it without limit. This is what makes GPU brush rendering fast enough for live drawing.
#4469: Simple gpu airbrush (open, 2026-08-21, stack #4444 tip)
The first GPU brush node. The airbrush node accepts styled strokes (color, diameter, hardness, flow) as input, renders them through a dedicated wgpu pipeline into a GPU texture, and returns that texture as raster output. Incremental rendering runs on top of the brush cache from Brush cache #4468. Brush kernels are baked to small textures and cached weakly alongside the pipeline. This PR is what everything else has been working towards: a relatively simple example brush that takes advantage of all the infrastructure I've built over the last 3 months.
Infrastructure work needed for the project
#4210: Extract pipelines from
WgpuExecutorinto scope-provided pipeline nodes (merged, 2026-06-07)Before this all GPU pipelines lived hardcoded inside the central
WgpuExecutor, so a node crate could not implement its own GPU pipeline. This PR moves pipelines into individual nodes whose output is provided to other nodes through the document scope. The airbrush defines its own pipeline node and receives the pipeline cache through a scope input.#4221: Add node macro support for injecting nodes into global scope (merged, 2026-06-10)
Adds
inject_scopesupport to the node macro. Nodes can declare that they should be injected into the network's global scope, making their output available as a scope input to any node without manual wiring. The airbrush's pipeline node uses exactly this mechanism.#4235: Resolve scopes before flattening network (merged, 2026-06-14)
Followup to Add node macro support for injecting nodes into global scope #4221. Moves scope resolution in the graph compiler to before network flattening, fixing resolution problems that surfaced once Extract pipelines from
WgpuExecutorinto scope-provided pipeline nodes #4210 started using scope injection for pipeline nodes.#4270: Desktop: Prefer Vulkan as the WGPU backend on Linux (merged, 2026-06-22)
OpenGL was not working correctly with brush nodes (empty textures, further investigation needed), so the desktop app now prefers Vulkan on Linux.
#4280: Change footprint to always be in physical instead of logical space (merged, 2026-06-24)
The render footprint (the resolution and transform a node renders at) was passed down in logical pixels, so on hiDPI displays upstream nodes rendered textures at the wrong size. Footprints are now always in physical pixels, with explicit conversion where logical space is really wanted. Brushes render strokes to textures sized by the footprint, so correct physical resolutions are a prerequisite for crisp strokes at any display scale.
#4284: Hide node parameters connected to a NodeInput::Scope in the Properties panel (merged, 2026-06-25)
Scope inputs are internal plumbing, not user-editable values, so the Properties panel now hides them. Without this, the airbrush node would show its pipeline input as a meaningless parameter to the user.
#4333: Fix lifetime of cached textures (merged, 2026-07-12)
Fixes a crash (a wgpu validation error) where a cached texture was destroyed while newly queued GPU work still referenced it. Part of making the texture cache reliable enough to build the brush's texture handling on.
#4375: Remove now obsolete
disable-registrationspecial case from node macro (merged, 2026-07-25)Removes a node macro special case that the scope and pipeline node work made unnecessary.
vello#1777: Destroy image textures before dropping them (merged upstream, 2026-07-30)
Upstream fix in the vello renderer. In browsers, the JavaScript garbage collector frees wgpu textures too late because it does not feel GPU memory pressure, so GPU memory fills up. Vello now destroys its image textures explicitly instead of waiting for garbage collection. Found while chasing GPU out-of-memory errors that would break long brush sessions, which create many textures.
#4446: Fix vello related GPU out of memory issues (open, 2026-08-17, stack #4444)
Pulls the vello#1777 workaround into Graphite and makes the blob IDs passed to vello stable across frames, so vello can reuse texture atlas slots instead of re-uploading every frame. Keeps GPU memory stable while brushes stream textures through the renderer.
#4447: Improve texture caching by allowing weak refs and more formats (open, 2026-08-17, stack #4444)
Extends the texture cache with weak references and more texture formats. A node can hold a texture weakly: the cache may evict it under memory pressure, and the node re-renders it on demand. The airbrush uses weak references for its baked kernel and stroke textures, trading re-render time for bounded GPU memory. Also fixes some GPU out-of-memory issues on web.
Other work during GSoC
#4163: Antialias checkered artboard background edges (merged, 2026-05-22)
#4165: Migrate fonts to be stored as resources (merged, 2026-05-23)
#4168: Define data types for ID-based resource handling (merged, 2026-05-25)
Introduces core data types for referring to embedded resources by stable IDs instead of passing raw data around.
#4176: Use ResourceId for referring to Resources (merged, 2026-05-26)
Switches resource references over to the new
ResourceIdtype introduced in Define data types for ID-based resource handling #4168.#4184: Fix CI profiling compile failure (merged, 2026-05-31)
#4187: Make Resource aware of its own hash (merged, 2026-05-31)
Resource now carries its own content hash, so consumers do not need to re-hash large blobs.
#4194: Introduce network request handler (merged, 2026-06-03)
#4201: Desktop: Disable UI acceleration by default on Linux (merged, 2026-06-05)
#4225: Desktop: Block network requests in CEF that are not explicitly allowlisted (merged, 2026-06-11)
#4232: Move
TaggedValue::ResourceHashto macro definition site (merged, 2026-06-12)#4254: Replace the dev server's
wasm-pack,cargo-watch, andconcurrentlytools with custom cargo-run tooling (merged, 2026-06-19)Replaces three third-party dev tools with an in-repo cargo-run tool that calls the underlying build steps directly. Fewer external dependencies, more control over the build, and readable commands. Made iterating on other tasks a lot faster.
#4256: Make the cargo-run tool auto-install its
wasm-optbuild dependency (merged, 2026-06-19)Followup to Replace the dev server's
wasm-pack,cargo-watch, andconcurrentlytools with custom cargo-run tooling #4254; contributors no longer need to installwasm-optmanually.#4266: Move branding and package install into cargo-run (merged, 2026-06-21)
#4281: Snap overlays to device instead of UI pixels on web (merged, 2026-06-24)
Web counterpart of an earlier desktop fix (Desktop: Snap overlays to physical instead of logical pixels #3493).
#4286: Desktop: Update CEF to 149 (merged, 2026-06-27)
#4296: Rework clipboard handling to carry embedded resources across documents (merged, 2026-06-30)
Copy and paste between documents now carries embedded resources (such as fonts and images) along, instead of leaving dangling references behind.
#4305: Desktop: Use dirty rects for UI texture uploads (merged, 2026-07-02)
#4317: Desktop: Fix crash caused by submitting work mid surface-reconfigure (merged, 2026-07-08)
Fixes a crash where GPU work was submitted while the window surface was being reconfigured, for example during resize. I discovered this while working on the first shader based brush rendering technique. When a brush node was part of the open document, Graphite was likely to crash when resizing.
#4321: Desktop: Isolate CEF-rendered UI into separate crate and process (merged, 2026-07-10)
Moves CEF into its own host process, controlled over IPC, with frames returned through shared memory or shared GPU textures. Fixes a whole class of problems that come with running CEF in-process. Increased crash CEF happyness and some drawing tablet input issues on macOS (never figured out the underlying problem) caused me to work on this.
#4322: Desktop: Handle keyboard shortcuts that are skipped by CEF on Mac (merged, 2026-07-10)
Handles keyboard shortcuts that CEF swallows on macOS. Depends on the process isolation from Desktop: Isolate CEF-rendered UI into separate crate and process #4321. Fixes On Mac, text selection keyboard shortcuts aren't all working (like Cmd+A) #3516.
#4323: More consistent document crate names (merged, 2026-07-10)
Renames the document crates to a consistent
document-*naming scheme.#4324: Desktop: Use custom CEF build on Linux to fix GPU accelerated UI (open, 2026-07-10)
If ever successful, this would gain back some performance recently lost to a CEF update (not a big degradation, but still worth trying and completing at some point).
#4348: Desktop: Stop compiling the editor into the wasm wrapper (merged, 2026-07-17)
Makes the editor an optional dependency of the wasm wrapper, so the desktop build no longer compiles the whole editor to WebAssembly alongside the native editor. Cuts desktop build time and made my life iterating on drawing tablet support and brush work with the desktop app way more comfortable.
#4351: Move file filter creation from frontend to editor (open, 2026-07-18)
#4352: Replace string-keyed attribute access with a typed attribute key API (open, 2026-07-18)
General data-model improvement; the brush nodes use this attribute system for stroke styling, but do not depend on the typed API. If this is merged, it would make writing nodes a little more ergonomic.
#4353: Fix node insertion crash caused by mismatched
DefinitionIdentifierserialization (merged, 2026-07-18)Fixes a regression introduced by Desktop: Stop compiling the editor into the wasm wrapper #4348.
winit#4658: clipboard data transfer api (open, 2026-08-05)
The next winit version currently breaks Graphite's clipboard capabilities on Wayland (explanation in the PR). I stepped in and implemented the planned clipboard handling using winit's data transfer model. Once this is merged, it allows us to upgrade winit and take advantage of better drawing tablet and touch support, so it is indirectly related to the drawing tablet work. The tablet support in winit itself was already done (by someone else) when GSoC started; the issues we found are mostly resolved upstream.
All reactions