#4 Cache textures for performance boost

Open
opened 1 year ago by kirsle · 0 comments
kirsle commented 1 year ago

To improve UI toolkit performance, widgets shouldn’t need to re-draw themselves from scratch every single frame. This is fine enough on desktop builds, but WASM builds quickly get bogged down with all the extra draw calls.

Consider the simple Button widget which incurs at least five draw ops per button per frame:

  1. Draw a solid black rectangle for the outline
  2. Draw a solid light grey rect for the highlight
  3. Draw a solid dark grey rect for the shadow
  4. Draw a solid grey rect for the button background color
  5. Draw the text for the button (possibly with additional draw calls for shadow or stroked text).

This ticket contains ideas and notes on how texture caching will work.

Prerequisite

The underlying go/render engine has a Texturer interface for holding cached textures (SDL2 or HTML Canvas), but the interface is currently optimized for holding onto generated PNG images.

The Texturer interface should be extended to support creating a “blank” texture and drawing to it manually in much the same way as the render.Engine interface itself: with functions like DrawLine(), DrawRect(), DrawText() and so on.

The UI toolkit can then direct its draw calls to a Texturer instead of directly to the Engine.

Implementation

High level overview is:

  • Each widget is responsible for its own texture cache.
  • Widgets would keep a “dirty” flag to invalidate the cache and re-draw itself from scratch.
  • Their Present() method would be updated to draw the cached texture if available or redraw from scratch if missing/dirty.

For example with the Label widget:

type Label struct {
    // set to true when the Label needs to re-draw and
    // invalidate its cached texture
    dirty bool
    
    // cached texture for the Label
    texture render.Texturer
}

func (w *Label) Present(e render.Engine, p render.Point) {
    tex := w.texture
    
    // if no cached texture OR dirty flag is set, draw the
    // texture from scratch.
    if tex == nil || w.dirty {
    	tex = e.NewTexture()
    	tex.DrawText(...)
    	w.texture = tex
    }
    
    // paste the texture onto the window
    e.Copy(tex, ...)
}

So for the best case scenario where a texture is already cached, each call to Present() just blits the texture to the screen in a single draw call.

The dirty flag would be set for cases such as:

  • If a TextVariable or IntVariable is used in a widget and the variable’s value has changed.
  • If the widget is reconfigured (changed border type or size, changed colors, etc.)

Since caching is up to the widgets, some widgets can maintain multiple cached states, for example Button might keep caches for their normal, hover, and pressed states whereas Label only needs one texture.

To improve UI toolkit performance, widgets shouldn't need to re-draw themselves from scratch every single frame. This is fine enough on desktop builds, but WASM builds quickly get bogged down with all the extra draw calls. Consider the simple Button widget which incurs at least five draw ops per button per frame: 1. Draw a solid black rectangle for the outline 2. Draw a solid light grey rect for the highlight 3. Draw a solid dark grey rect for the shadow 4. Draw a solid grey rect for the button background color 5. Draw the text for the button (possibly with additional draw calls for shadow or stroked text). This ticket contains ideas and notes on how texture caching will work. ### Prerequisite The underlying [go/render](https://git.kirsle.net/go/render) engine has a Texturer interface for holding cached textures (SDL2 or HTML Canvas), but the interface is currently optimized for holding onto generated PNG images. The Texturer interface should be extended to support creating a "blank" texture and drawing to it manually in much the same way as the render.Engine interface itself: with functions like DrawLine(), DrawRect(), DrawText() and so on. The UI toolkit can then direct its draw calls to a Texturer instead of directly to the Engine. ### Implementation High level overview is: * Each widget is responsible for its own texture cache. * Widgets would keep a "dirty" flag to invalidate the cache and re-draw itself from scratch. * Their Present() method would be updated to draw the cached texture if available or redraw from scratch if missing/dirty. For example with the Label widget: ```go type Label struct { // set to true when the Label needs to re-draw and // invalidate its cached texture dirty bool // cached texture for the Label texture render.Texturer } func (w *Label) Present(e render.Engine, p render.Point) { tex := w.texture // if no cached texture OR dirty flag is set, draw the // texture from scratch. if tex == nil || w.dirty { tex = e.NewTexture() tex.DrawText(...) w.texture = tex } // paste the texture onto the window e.Copy(tex, ...) } ``` So for the best case scenario where a texture is already cached, each call to Present() just blits the texture to the screen in a single draw call. The `dirty` flag would be set for cases such as: * If a TextVariable or IntVariable is used in a widget and the variable's value has changed. * If the widget is reconfigured (changed border type or size, changed colors, etc.) Since caching is up to the widgets, some widgets can maintain multiple cached states, for example Button might keep caches for their normal, hover, and pressed states whereas Label only needs one texture.
kirsle added the
enhancement
label 1 year ago
Sign in to join this conversation.
No Milestone
No Assignees
1 Participants
Notifications
Due Date

No due date set.

Dependencies

This issue currently doesn't have any dependencies.

Loading…
There is no content yet.