Files
Aethera/绘制优化.md
T
2026-08-26 18:06:48 +08:00

8.0 KiB
Raw Blame History

可以实现,而且你这个“天然分区并行”的场景很适合这么做。但要把边界说清楚:这不是 Blend2D 官方提供的 BLImageView 功能,而是利用 BLImage::create_from_data() 给同一块外部 framebuffer 建多个独立的 BLImage 包装。Blend2D 官方明确支持外部 pixel_data + stride 创建 image。 (Blend2D)

比如最终 framebuffer 是 1920×1080 PRGB32

struct Framebuffer {
    uint8_t* data;
    int width;
    int height;
    intptr_t stride;
};

BLImage make_view(
    Framebuffer& fb,
    int x,
    int y,
    int width,
    int height)
{
    BLImage image;
    uint8_t* ptr = fb.data + y * fb.stride + x * 4;

    image.create_from_data(
        width,
        height,
        BL_FORMAT_PRGB32,
        ptr,
        fb.stride,
        BL_DATA_ACCESS_RW);

    return image;
}

比如:

1920 × 1080 framebuffer

┌──────────────┬──────────────┐
│ view A       │ view B       │
│ 960×540      │ 960×540      │
├──────────────┼──────────────┤
│ view C       │ view D       │
│ 960×540      │ 960×540      │
└──────────────┴──────────────┘

然后:

auto a = make_view(fb, 0,   0,   960, 540);
auto b = make_view(fb, 960, 0,   960, 540);
auto c = make_view(fb, 0,   540, 960, 540);
auto d = make_view(fb, 960, 540, 960, 540);

Taskflow

worker 0 → BLContext(a)
worker 1 → BLContext(b)
worker 2 → BLContext(c)
worker 3 → BLContext(d)

四个 context 都可以是同步单线程 context:

BLContext ctx(a);

Blend2D 文档说明这种普通 BLContext(image) 就是 single-threaded synchronous rendering。 (Blend2D)

最关键的是为什么它可以绕开“一个 image 只能有一个 renderer”。

因为 Blend2D 的 exclusive-access 管理针对的是它自己的 image data 对象。BLContext::begin() 会增加目标 image 的 writer count,并保证同一个 image data 不被另一个 renderer 同时使用。 (Blend2D)

而你这里创建的是:

BLImage A → external-memory wrapper A
BLImage B → external-memory wrapper B
BLImage C → external-memory wrapper C

它们不是:

BLImage b = a;

这种 weak copy。

每次都是独立调用:

create_from_data(...)

所以 Blend2D 看到的是三个不同 image data 对象。

只是:

物理内存:

A.data ─┐
B.data ─┼→ 同一个 framebuffer allocation
C.data ─┘

Blend2D并不知道这些外部内存背后属于同一 allocation。

因此真正的线程安全责任就在你这里。

你必须保证:

A 实际写入地址集合
∩
B 实际写入地址集合
=
∅

如果做到这一点,从 C++ 内存访问角度就是正常的并行写不同内存。

你需要特别注意几个问题。

第一,stride 必须是父 framebuffer 的 stride。

例如:

parent width = 1920
PRGB32 = 4 bytes
stride = 7680

B 虽然只有 960 宽:

pixel_data = base + 960 * 4;

但:

stride = 1920 * 4;

仍然不能写成:

stride = 960 * 4;

否则下一行就跑错位置。

BLImageData 本身也是这么定义的:pixel_data 是图像左上角,stride 是每行之间的字节跨度。 (Blend2D)

第二,view 范围必须真的落在 framebuffer 内:

x >= 0
y >= 0

x + width  <= framebuffer.width
y + height <= framebuffer.height

这个检查我会放在你创建 surface/view 的最外层,而不是每次绘制都检查。

第三,也是比较重要的:不要让每个 view 都释放同一个 framebuffer。

create_from_data() 有:

destroy_func
user_data

用于外部内存生命周期管理。 (Blend2D)

如果你这样干:

view A destroy → free(framebuffer)
view B destroy → free(framebuffer)

就是 double-free。

你这种结构最简单的是:

image.create_from_data(
    ...,
    BL_DATA_ACCESS_RW,
    nullptr,
    nullptr);

由你的 framebuffer owner 自己负责生命周期:

Framebuffer
    owns memory

    ├── ImageView A
    ├── ImageView B
    ├── ImageView C
    └── ImageView D

保证:

所有 ImageView / BLContext 销毁
        ↓
Framebuffer 才销毁

如果必须跨生命周期,就自己搞共享 owner/refcount。

第四,不要 weak-copy view 再期待两个 renderer 并行:

BLImage a = make_view(...);
BLImage b = a;

这不是两个 view。

这是共享同一个 Blend2D image data。Blend2D 的 BLImage copy 是 weak-copy,会共享底层 image data。 (Blend2D)

应该每个 region 独立:

BLImage a;
a.create_from_data(...);

BLImage b;
b.create_from_data(...);

第五,我建议你的 view 自己建立局部坐标系。

比如 Plot B 在最终 framebuffer

x = 960
y = 0
w = 960
h = 540

但是它拿到的 BLImage 是:

0 .. 959
0 .. 539

所以 Plot 内部正常画:

ctx.fill_rect(BLRect(0, 0, 960, 540), ...);

不需要知道自己在父 framebuffer 里是 (960, 0)

这对你的 Scene/Plot 抽象非常干净:

Parent framebuffer coordinates
          ↓
Surface/View
          ↓
local coordinates
          ↓
Plot renderer

还有一个容易担心但实际上比较好的点:Blend2D 会裁剪到当前 image target。

所以一个 960×540 的 view,即使你的 path 坐标跑到:

x = 1500

Blend2D target 本身只有:

960 × 540

正常 raster 不应该越过 target memory 定义的逻辑宽度去写相邻区域。

也就是说 view 本身实际上同时给了你一个天然 clip boundary。

所以我会把你的最终结构做成:

class Surface {
public:
    BLImage image;
    int x;
    int y;
    int width;
    int height;
};

然后:

Framebuffer
   │
   ├── Surface Plot0
   │      └── BLImage(external framebuffer region)
   │
   ├── Surface Plot1
   │      └── BLImage(external framebuffer region)
   │
   └── Surface Plot2
          └── BLImage(external framebuffer region)

Taskflow

                    framebuffer

             ┌──────────┬──────────┐
             │ Plot A   │ Plot B   │
             ├──────────┼──────────┤
             │ Plot C   │ Plot D   │
             └──────────┴──────────┘
                  ▲          ▲
                  │          │
               Task A     Task B

这种情况下不存在:

Image A
Image B
Image C
    ↓
composite

每个任务第一次 rasterize 就已经直接落到最终 framebuffer。

不过我要保留一个工程上的判断:

这是利用 Blend2D external-image API 实现的物理 framebuffer 分区,并不是 Blend2D 文档明确承诺的“多个 external BLImage 指向同一 allocation 的不同区域并发渲染”高级用法。

Blend2D 官方承诺的是 external memory image 本身可用,以及一个 context 对其 image data 独占;它不会理解你几个独立 external image 的 backing allocations 是否重叠。 (Blend2D)

所以我会把它当成:

可实现
+
原理成立
+
需要你自己严格保证 region 不重叠
+
最好做一次 TSAN + 压力测试 + framebuffer guard 测试

对于你这种“一个大图天然分成多个 Plot”的场景,我认为值得做,而且比“各画一张临时图最后再 blit”更符合你追求后台性能的方向。