framebuffer_session: accessors for buffer surfaces

This patch adds central and safe utilities for accessing the distinct
parts of the virtual framebuffer to relieve clients from pointer
calculations.

Issue #5351
This commit is contained in:
Norman Feske 2024-09-25 16:29:06 +02:00 committed by Christian Helmuth
parent 388218a3f9
commit 5b4e1915d8
15 changed files with 206 additions and 242 deletions

View File

@ -15,14 +15,8 @@
#define _INCLUDE__GEMS__GUI_BUFFER_H_
/* Genode includes */
#include <base/ram_allocator.h>
#include <gui_session/connection.h>
#include <base/attached_dataspace.h>
#include <base/attached_ram_dataspace.h>
#include <os/surface.h>
#include <os/pixel_alpha8.h>
#include <os/pixel_rgb888.h>
#include <blit/painter.h>
struct Gui_buffer : Genode::Noncopyable
@ -38,76 +32,36 @@ struct Gui_buffer : Genode::Noncopyable
using size_t = Genode::size_t;
using uint8_t = Genode::uint8_t;
Genode::Ram_allocator &ram;
Genode::Region_map &rm;
Genode::Ram_allocator &_ram;
Genode::Region_map &_rm;
Gui::Connection &gui;
Gui::Connection &_gui;
Framebuffer::Mode const mode;
/*
* Make the GUI mode twice as high as the requested mode. The upper part
* of the GUI framebuffer contains the front buffer, the lower part
* contains the back buffer.
*/
Framebuffer::Mode const _gui_mode { .area = { mode.area.w, mode.area.h*2 },
.alpha = mode.alpha };
Genode::Surface_window const _backbuffer { .y = mode.area.h, .h = mode.area.h };
Pixel_rgb888 const reset_color;
/**
* Return dataspace capability for virtual framebuffer
*/
Genode::Dataspace_capability _ds_cap(Gui::Connection &gui)
{
/*
* Setup virtual framebuffer, the upper part containing the front
* buffer, the lower part containing the back buffer.
*/
gui.buffer({ .area = { mode.area.w, mode.area.h*2 },
.alpha = mode.alpha });
return gui.framebuffer.dataspace();
}
Genode::Attached_dataspace _fb_ds { rm, _ds_cap(gui) };
size_t _pixel_num_bytes() const { return size().count()*sizeof(Pixel_rgb888); }
size_t _alpha_num_bytes() const { return mode.alpha ? size().count() : 0; }
size_t _input_num_bytes() const { return mode.alpha ? size().count() : 0; }
void _with_pixel_ptr(auto const &fn)
{
/* skip pixel front buffer */
uint8_t * const ptr = _fb_ds.local_addr<uint8_t>() + _pixel_num_bytes();
fn((Pixel_rgb888 *)ptr);
}
void _with_alpha_ptr(auto const &fn)
{
if (!mode.alpha)
return;
/* skip pixel front buffer, pixel back buffer, and alpha front buffer */
uint8_t * const ptr = _fb_ds.local_addr<uint8_t>()
+ _pixel_num_bytes()*2 + _alpha_num_bytes();
fn((Pixel_alpha8 *)ptr);
}
void _with_input_ptr(auto const &fn)
{
if (!mode.alpha)
return;
/* skip pixel buffers, alpha buffers, and input front buffer */
uint8_t * const ptr = _fb_ds.local_addr<uint8_t>()
+ _pixel_num_bytes()*2 + _alpha_num_bytes()*2 + _input_num_bytes();
fn(ptr);
}
Genode::Attached_dataspace _fb_ds {
_rm, ( _gui.buffer(_gui_mode), _gui.framebuffer.dataspace() ) };
enum class Alpha { OPAQUE, ALPHA };
static Genode::Color default_reset_color()
{
/*
* Do not use black by default to limit the bleeding of black into
* antialiased drawing operations applied onto an initially transparent
* background.
*/
return Genode::Color(127, 127, 127, 255);
}
/*
* Do not use black by default to limit the bleeding of black into
* antialiased drawing operations applied onto an initially transparent
* background.
*/
static constexpr Genode::Color default_reset_color = { 127, 127, 127, 255 };
/**
* Constructor
@ -115,9 +69,9 @@ struct Gui_buffer : Genode::Noncopyable
Gui_buffer(Gui::Connection &gui, Area size,
Genode::Ram_allocator &ram, Genode::Region_map &rm,
Alpha alpha = Alpha::ALPHA,
Genode::Color reset_color = default_reset_color())
Genode::Color reset_color = default_reset_color)
:
ram(ram), rm(rm), gui(gui),
_ram(ram), _rm(rm), _gui(gui),
mode({ .area = { Genode::max(1U, size.w),
Genode::max(1U, size.h) },
.alpha = (alpha == Alpha::ALPHA) }),
@ -133,16 +87,21 @@ struct Gui_buffer : Genode::Noncopyable
void with_alpha_surface(auto const &fn)
{
_with_alpha_ptr([&] (Pixel_alpha8 *ptr) {
Alpha_surface alpha { ptr, size() };
fn(alpha); });
if (!_gui_mode.alpha) {
Alpha_surface dummy { nullptr, Gui::Area { } };
fn(dummy);
return;
}
_gui_mode.with_alpha_surface(_fb_ds, [&] (Alpha_surface &surface) {
surface.with_window(_backbuffer, [&] (Alpha_surface &surface) {
fn(surface); }); });
}
void with_pixel_surface(auto const &fn)
{
_with_pixel_ptr([&] (Pixel_rgb888 *ptr) {
Pixel_surface pixel { ptr, size() };
fn(pixel); });
_gui_mode.with_pixel_surface(_fb_ds, [&] (Pixel_surface &surface) {
surface.with_window(_backbuffer, [&] (Pixel_surface &surface) {
fn(surface); }); });
}
void apply_to_surface(auto const &fn)
@ -155,7 +114,7 @@ struct Gui_buffer : Genode::Noncopyable
void reset_surface()
{
with_alpha_surface([&] (Alpha_surface &alpha) {
Genode::memset(alpha.addr(), 0, _alpha_num_bytes()); });
Genode::memset(alpha.addr(), 0, alpha.size().count()); });
with_pixel_surface([&] (Pixel_surface &pixel) {
@ -169,20 +128,28 @@ struct Gui_buffer : Genode::Noncopyable
void _update_input_mask()
{
_with_alpha_ptr([&] (Pixel_alpha8 const * const alpha_ptr) {
_with_input_ptr([&] (uint8_t *dst) {
with_alpha_surface([&] (Alpha_surface &alpha) {
/*
* Set input mask for all pixels where the alpha value is above
* a given threshold. The threshold is defined such that
* typical drop shadows are below the value.
*/
uint8_t const threshold = 100;
uint8_t const * src = (uint8_t const *)alpha_ptr;
size_t const num_pixels = size().count();
using Input_surface = Genode::Surface<Genode::Pixel_input8>;
for (unsigned i = 0; i < num_pixels; i++)
*dst++ = (*src++) > threshold;
_gui_mode.with_input_surface(_fb_ds, [&] (Input_surface &input) {
input.with_window(_backbuffer, [&] (Input_surface &input) {
uint8_t const * src = (uint8_t *)alpha.addr();
uint8_t * dst = (uint8_t *)input.addr();
/*
* Set input mask for all pixels where the alpha value is
* above a given threshold. The threshold is defined such
* that typical drop shadows are below the value.
*/
uint8_t const threshold = 100;
size_t const num_pixels = Genode::min(alpha.size().count(),
input.size().count());
for (unsigned i = 0; i < num_pixels; i++)
*dst++ = (*src++) > threshold;
});
});
});
}
@ -192,7 +159,7 @@ struct Gui_buffer : Genode::Noncopyable
_update_input_mask();
/* copy lower part of virtual framebuffer to upper part */
gui.framebuffer.blit({ { 0, int(size().h) }, size() }, { 0, 0 });
_gui.framebuffer.blit({ { 0, int(size().h) }, size() }, { 0, 0 });
}
};

View File

@ -66,11 +66,6 @@ struct Backdrop::Main
Attached_dataspace fb_ds;
Genode::size_t surface_num_bytes() const
{
return size().count()*mode.bytes_per_pixel();
}
Attached_ram_dataspace surface_ds;
/**
@ -79,7 +74,7 @@ struct Backdrop::Main
Buffer(Genode::Env &env, Gui::Connection &gui, Framebuffer::Mode mode)
: gui(gui), mode(mode),
fb_ds(env.rm(), _ds_cap(gui)),
surface_ds(env.ram(), env.rm(), surface_num_bytes())
surface_ds(env.ram(), env.rm(), mode.num_bytes())
{ }
/**
@ -100,11 +95,10 @@ struct Backdrop::Main
void flush_surface()
{
/* blit back to front buffer */
blit(surface_ds.local_addr<void>(),
(unsigned)surface_num_bytes(),
fb_ds.local_addr<void>(),
(unsigned)surface_num_bytes(),
(unsigned)surface_num_bytes(), 1);
unsigned const num_bytes = unsigned(mode.num_bytes());
blit(surface_ds.local_addr<void>(), num_bytes,
fb_ds.local_addr<void>(), num_bytes,
num_bytes, 1);
}
};

View File

@ -286,8 +286,6 @@ class Decorator::Window : public Window_base, public Animator::Item
});
buffer.flush_surface();
buffer.gui.framebuffer.refresh({ { 0, 0 }, buffer.size() });
}
void _repaint_decorations()

View File

@ -19,6 +19,8 @@
#include <dataspace/capability.h>
#include <session/session.h>
#include <os/surface.h>
#include <os/pixel_rgb888.h>
#include <os/pixel_alpha8.h>
namespace Framebuffer {
@ -36,9 +38,66 @@ namespace Framebuffer {
Area area;
bool alpha;
size_t bytes_per_pixel() const { return 4; }
void print(Output &out) const { Genode::print(out, area); }
/*
* If using an alpha channel, the alpha buffer follows the pixel
* buffer. The alpha buffer is followed by an input-mask buffer.
*
* The input-mask buffer contains a byte value per texture pixel,
* which describes the policy of handling user input referring to the
* pixel. If set to zero, the input is passed through the view such
* that it can be handled by one of the subsequent views in the view
* stack. If set to one, the input is consumed by the view.
*/
void with_pixel_surface(auto &ds, auto const &fn) const
{
Surface<Pixel_rgb888> surface { ds.bytes(), area };
fn(surface);
}
void with_alpha_bytes(auto &ds, auto const &fn) const
{
if (!alpha)
return;
size_t const offset = area.count()*sizeof(Pixel_rgb888);
ds.bytes().with_skipped_bytes(offset, [&] (Byte_range_ptr const &bytes) {
fn(bytes); });
}
void with_alpha_surface(auto &ds, auto const &fn) const
{
with_alpha_bytes(ds, [&] (Byte_range_ptr const &bytes) {
Surface<Pixel_alpha8> surface { bytes, area };
fn(surface); });
}
void with_input_bytes(auto &ds, auto const &fn) const
{
if (!alpha)
return;
size_t const offset = area.count()*sizeof(Pixel_rgb888) + area.count();
ds.bytes().with_skipped_bytes(offset, [&] (Byte_range_ptr const &bytes) {
fn(bytes); });
}
void with_input_surface(auto &ds, auto const &fn) const
{
with_input_bytes(ds, [&] (Byte_range_ptr const &bytes) {
Surface<Pixel_input8> surface { bytes, area };
fn(surface); });
}
size_t num_bytes() const
{
size_t const bytes_per_pixel =
sizeof(Pixel_rgb888) + alpha*(sizeof(Pixel_alpha8) + sizeof(Pixel_input8));
return area.count()*bytes_per_pixel;
}
};
struct Transfer

View File

@ -274,7 +274,7 @@ struct Gui::Session : Genode::Session
* If alpha blending is used, each pixel requires an additional byte
* for the alpha value and a byte holding the input mask.
*/
return (mode.bytes_per_pixel() + 2*mode.alpha)*mode.area.count();
return (sizeof(Pixel_rgb888) + 2*mode.alpha)*mode.area.count();
}

View File

@ -126,15 +126,32 @@ class Genode::Surface : public Surface_base
PT *_addr; /* base address of pixel buffer */
static Area _sanitized(Area area, size_t const num_bytes)
{
/* prevent division by zero */
if (area.w == 0)
return { };
size_t const bytes_per_line = area.w*sizeof(PT);
return { .w = area.w,
.h = unsigned(min(num_bytes/bytes_per_line, area.h)) };
}
public:
PT *addr() { return _addr; }
/**
* Constructor
/*
* \deprecated
*/
Surface(PT *addr, Area size)
: Surface_base(size, PT::format()), _addr(addr) { }
Surface(PT *addr, Area size) : Surface_base(size, PT::format()), _addr(addr) { }
Surface(Byte_range_ptr const &bytes, Area const area)
:
Surface_base(_sanitized(area, bytes.num_bytes), PT::format()),
_addr((PT *)bytes.start)
{ }
/**
* Call 'fn' with a sub-window surface as argument

View File

@ -25,33 +25,22 @@
namespace Nitpicker { class Buffer; }
class Nitpicker::Buffer
struct Nitpicker::Buffer : private Attached_ram_dataspace
{
private:
/**
* Constructor - allocate and map dataspace for virtual frame buffer
*
* \throw Out_of_ram
* \throw Out_of_caps
* \throw Region_map::Region_conflict
*/
Buffer(Ram_allocator &ram, Region_map &rm, size_t num_bytes)
:
Attached_ram_dataspace(ram, rm, num_bytes)
{ }
Area _size;
Attached_ram_dataspace _ram_ds;
public:
/**
* Constructor - allocate and map dataspace for virtual frame buffer
*
* \throw Out_of_ram
* \throw Out_of_caps
* \throw Region_map::Region_conflict
*/
Buffer(Ram_allocator &ram, Region_map &rm, Area size, size_t bytes)
:
_size(size), _ram_ds(ram, rm, bytes)
{ }
/**
* Accessors
*/
Ram_dataspace_capability ds_cap() const { return _ram_ds.cap(); }
Area size() const { return _size; }
void *local_addr() const { return _ram_ds.local_addr<void>(); }
using Attached_ram_dataspace::bytes;
using Attached_ram_dataspace::cap;
};

View File

@ -15,8 +15,6 @@
#define _CHUNKY_TEXTURE_H_
/* Genode includes */
#include <os/pixel_rgb888.h>
#include <os/pixel_alpha8.h>
#include <blit/painter.h>
/* local includes */
@ -26,69 +24,36 @@ namespace Nitpicker { template <typename> class Chunky_texture; }
template <typename PT>
class Nitpicker::Chunky_texture : public Buffer, public Texture<PT>
class Nitpicker::Chunky_texture : Buffer, public Texture<PT>
{
private:
Framebuffer::Mode const _mode;
/**
* Return base address of alpha channel or 0 if no alpha channel exists
*/
unsigned char *_alpha_base(Framebuffer::Mode mode)
static uint8_t *_alpha_base(Buffer &buffer, Framebuffer::Mode mode)
{
if (!mode.alpha) return nullptr;
/* alpha values come right after the pixel values */
return (unsigned char *)local_addr()
+ calc_num_bytes({ .area = mode.area, .alpha = false });
}
Area _area() const { return Texture<PT>::size(); }
void _with_pixel_surface(auto const &fn)
{
Surface<Pixel_rgb888> pixel { (Pixel_rgb888 *)local_addr(), _area() };
fn(pixel);
}
static void _with_alpha_ptr(auto &obj, auto const &fn)
{
Pixel_alpha8 * const ptr = (Pixel_alpha8 *)(obj.Texture<PT>::alpha());
if (ptr)
fn(ptr);
}
void _with_alpha_surface(auto const &fn)
{
_with_alpha_ptr(*this, [&] (Pixel_alpha8 * const ptr) {
Surface<Pixel_alpha8> alpha { ptr, _area() };
fn(alpha); });
uint8_t *result = nullptr;
mode.with_alpha_bytes(buffer, [&] (Byte_range_ptr const &bytes) {
result = (uint8_t *)bytes.start; });
return result;
}
void _with_alpha_texture(auto const &fn) const
{
_with_alpha_ptr(*this, [&] (Pixel_alpha8 * const ptr) {
Texture<Pixel_alpha8> texture { ptr, nullptr, _area() };
Buffer const &buffer = *this;
_mode.with_alpha_bytes(buffer, [&] (Byte_range_ptr const &bytes) {
Texture<Pixel_alpha8> texture { (Pixel_alpha8 *)bytes.start, nullptr, _mode.area };
fn(texture); });
}
static void _with_input_ptr(auto &obj, auto const &fn)
{
Pixel_alpha8 * const ptr = (Pixel_alpha8 *)(obj.input_mask_buffer());
if (ptr)
fn(ptr);
}
void _with_input_surface(auto const &fn)
{
_with_input_ptr(*this, [&] (Pixel_alpha8 * const ptr) {
Surface<Pixel_alpha8> input { ptr, _area() };
fn(input); });
}
void _with_input_texture(auto const &fn) const
{
_with_input_ptr(*this, [&] (Pixel_alpha8 * const ptr) {
Texture<Pixel_alpha8> texture { ptr, nullptr, _area() };
Buffer const &buffer = *this;
_mode.with_input_bytes(buffer, [&] (Byte_range_ptr const &bytes) {
Texture<Pixel_input8> texture { (Pixel_input8 *)bytes.start, nullptr, _mode.area };
fn(texture); });
}
@ -102,47 +67,36 @@ class Nitpicker::Chunky_texture : public Buffer, public Texture<PT>
public:
/**
* Constructor
*/
using Buffer::cap;
Chunky_texture(Ram_allocator &ram, Region_map &rm, Framebuffer::Mode mode)
:
Buffer(ram, rm, mode.area, calc_num_bytes(mode)),
Texture<PT>((PT *)local_addr(), _alpha_base(mode), mode.area)
Buffer(ram, rm, mode.num_bytes()),
Texture<PT>((PT *)Buffer::bytes().start, _alpha_base(*this, mode), mode.area),
_mode(mode)
{ }
static size_t calc_num_bytes(Framebuffer::Mode mode)
void with_input_mask(auto const &fn) const
{
/*
* If using an alpha channel, the alpha buffer follows the
* pixel buffer. The alpha buffer is followed by an input
* mask buffer. Hence, we have to account one byte per
* alpha value and one byte for the input mask value.
*/
size_t bytes_per_pixel = sizeof(PT) + (mode.alpha ? 2 : 0);
return bytes_per_pixel*mode.area.count();
}
uint8_t const *input_mask_buffer() const
{
if (!Texture<PT>::alpha()) return 0;
/* input-mask values come right after the alpha values */
Framebuffer::Mode const mode { .area = _area(), .alpha = false };
return (uint8_t const *)local_addr() + calc_num_bytes(mode) + _area().count();
Buffer const &buffer = *this;
_mode.with_input_bytes(buffer, [&] (Byte_range_ptr const &bytes) {
Const_byte_range_ptr const_bytes { bytes.start, bytes.num_bytes };
fn(const_bytes); });
}
void blit(Rect from, Point to)
{
_with_pixel_surface([&] (Surface<Pixel_rgb888> &surface) {
Buffer &buffer = *this;
_mode.with_pixel_surface(buffer, [&] (Surface<Pixel_rgb888> &surface) {
_blit_channel(surface, *this, from, to); });
_with_alpha_surface([&] (Surface<Pixel_alpha8> &surface) {
_mode.with_alpha_surface(buffer, [&] (Surface<Pixel_alpha8> &surface) {
_with_alpha_texture([&] (Texture<Pixel_alpha8> &texture) {
_blit_channel(surface, texture, from, to); }); });
_with_input_surface([&] (Surface<Pixel_alpha8> &surface) {
_with_input_texture([&] (Texture<Pixel_alpha8> &texture) {
_mode.with_input_surface(buffer, [&] (Surface<Pixel_input8> &surface) {
_with_input_texture([&] (Texture<Pixel_input8> &texture) {
_blit_channel(surface, texture, from, to); }); });
}
};

View File

@ -421,7 +421,7 @@ void Gui_session::focus(Capability<Gui::Session> session_cap)
Dataspace_capability Gui_session::realloc_buffer(Framebuffer::Mode mode)
{
Ram_quota const next_buffer_size { Chunky_texture<Pixel>::calc_num_bytes(mode) };
Ram_quota const next_buffer_size { mode.num_bytes() };
Ram_quota const orig_buffer_size { _buffer_size };
/*
@ -440,7 +440,6 @@ Dataspace_capability Gui_session::realloc_buffer(Framebuffer::Mode mode)
}
_buffer_size = 0;
_input_mask = nullptr;
Ram_quota const temporary_ram_upgrade = _texture.valid()
? next_buffer_size : Ram_quota{0};
@ -470,7 +469,6 @@ Dataspace_capability Gui_session::realloc_buffer(Framebuffer::Mode mode)
}
_buffer_size = next_buffer_size.value;
_input_mask = _texture.input_mask_buffer();
return _texture.dataspace();
}

View File

@ -94,17 +94,6 @@ class Nitpicker::Gui_session : public Session_object<Gui::Session>,
Domain_registry::Entry const *_domain = nullptr;
View *_background = nullptr;
/*
* The input mask buffer containing a byte value per texture pixel,
* which describes the policy of handling user input referring to the
* pixel. If set to zero, the input is passed through the view such
* that it can be handled by one of the subsequent views in the view
* stack. If set to one, the input is consumed by the view. If
* 'input_mask' is a null pointer, user input is unconditionally
* consumed by the view.
*/
unsigned char const *_input_mask = nullptr;
bool _visible = true;
Sliced_heap _session_alloc;
@ -289,20 +278,22 @@ class Nitpicker::Gui_session : public Session_object<Gui::Session>,
bool origin_pointer() const override { return _domain && _domain->origin_pointer(); }
/**
* Return input mask value at specified buffer position
*/
unsigned char input_mask_at(Point p) const override
bool input_mask_at(Point const p) const override
{
if (!_input_mask || !_texture.valid()) return 0;
bool result = false;
_texture.with_input_mask([&] (Const_byte_range_ptr const &bytes) {
/* check boundaries */
if ((unsigned)p.x >= _texture.size().w
|| (unsigned)p.y >= _texture.size().h)
return 0;
unsigned const x = p.x % _texture.size().w,
y = p.y % _texture.size().h;
return _input_mask[p.y*_texture.size().w + p.x];
size_t const offset = y*_texture.size().w + x;
if (offset < bytes.num_bytes)
result = bytes.start[offset];
});
return result;
}
void submit_input_event(Input::Event e) override;

View File

@ -105,13 +105,13 @@ class Nitpicker::Resizeable_texture
Dataspace_capability dataspace()
{
return valid() ? _textures[_current]->ds_cap() : Ram_dataspace_capability();
return valid() ? _textures[_current]->cap() : Ram_dataspace_capability();
}
unsigned char const *input_mask_buffer() const
void with_input_mask(auto const &fn) const
{
return valid() ? _textures[_current]->input_mask_buffer()
: nullptr;
if (valid())
_textures[_current]->with_input_mask(fn);
}
void blit(Rect from, Point to)

View File

@ -18,7 +18,6 @@
#include <util/xml_node.h>
#include <util/color.h>
#include <base/allocator.h>
#include <os/pixel_rgb888.h>
#include <gui_session/gui_session.h>
namespace Nitpicker {

View File

@ -11,8 +11,6 @@
* under the terms of the GNU Affero General Public License version 3.
*/
#include <os/pixel_rgb888.h>
#include <nitpicker_gfx/texture_painter.h>
#include <nitpicker_gfx/box_painter.h>

View File

@ -82,7 +82,7 @@ struct Nitpicker::View_owner : Interface
/**
* Return input-mask value at given position
*/
virtual unsigned char input_mask_at(Point) const { return 0; }
virtual bool input_mask_at(Point) const { return false; }
virtual void submit_input_event(Input::Event) { }

View File

@ -107,7 +107,7 @@ struct Blit_test : Test
{
unsigned kib = 0;
uint64_t const start_ms = timer.elapsed_ms();
unsigned const w = (unsigned)(fb_mode.area.w * fb_mode.bytes_per_pixel());
unsigned const w = unsigned(fb_mode.area.w * sizeof(Pixel_rgb888));
unsigned const h = fb_mode.area.h;
for (unsigned i = 0; timer.elapsed_ms() - start_ms < DURATION_MS; i++) {
blit(buf[i % 2], w, fb_ds.local_addr<char>(), w, w, h);
@ -125,7 +125,7 @@ struct Unaligned_blit_test : Test
{
unsigned kib = 0;
uint64_t const start_ms = timer.elapsed_ms();
unsigned const w = (unsigned)(fb_mode.area.w * fb_mode.bytes_per_pixel());
unsigned const w = unsigned(fb_mode.area.w * sizeof(Pixel_rgb888));
unsigned const h = fb_mode.area.h;
for (unsigned i = 0; timer.elapsed_ms() - start_ms < DURATION_MS; i++) {
blit(buf[i % 2] + 2, w, fb_ds.local_addr<char>() + 2, w, w - 2, h);