mvdocs

# 图像存储、访问与归还生命周期

目标设计与 API 草案,尚未实施。总体边界和推进状态见设计概述。

本文负责存储、所有权、授予、状态、初始化、native pin 与真实归还。类型化临时图像的声明见图像模型,访问语义以本文为准。

关联:图像模型 · ToolFlow · 采集 · 停止

# 存储工厂、生命周期与真实归还

public enum ImageBufferAccess
{
    ReadOnly = 0,
    ReadWrite = 1
}

public enum ImageBufferInitialization
{
    Uninitialized = 0,
    Initialized = 1
}

public enum ImageBufferReturnState
{
    Active = 0,
    Returning = 1,
    Returned = 2,
    ReturnFailed = 3
}

public sealed class ImageVersionMismatchException : InvalidOperationException
{
    public ImageContentVersion ExpectedVersion { get; }
    public ImageContentVersion ActualVersion { get; }
}

public sealed class ImageBuffer : IDisposable
{
    public static ImageBuffer Rent(
        int minimumCapacity,
        MemoryPool<byte>? pool = null);

    public static ImageBuffer TakeOwnership(
        IMemoryOwner<byte> owner,
        ImageBufferAccess access = ImageBufferAccess.ReadWrite,
        ImageBufferInitialization initialization = ImageBufferInitialization.Initialized,
        long? retainedByteLength = null);

    public static ImageBuffer ImportNative(
        nint address,
        long capacity,
        ImageBufferAccess access,
        Func<ValueTask> releaseAsync,
        ImageBufferInitialization initialization = ImageBufferInitialization.Initialized,
        long? retainedByteLength = null);

    public Guid BufferId { get; }
    public long Capacity { get; }
    public long RetainedByteLength { get; }
    public ImageBufferAccess Access { get; }
    public ImageBufferReturnState ReturnState { get; }
    public Task ReturnCompleted { get; }

    public ImageBuffer Share();
    public ValueTask<ImagePixelReader> OpenReadAsync(
        CancellationToken cancellationToken = default);
    public ValueTask<ImagePixelReader> OpenReadAsync(
        ImageContentVersion expectedVersion,
        CancellationToken cancellationToken = default);
    public ValueTask<ImagePixelWriter> OpenWriteAsync(
        CancellationToken cancellationToken = default);
    public ValueTask<ImagePixelWriter> OpenWriteAsync(
        ImageContentVersion expectedVersion,
        CancellationToken cancellationToken = default);
    public void Dispose();
}

# 工厂约束

工厂 初始内容 接管点与失败处理
Rent 未初始化、可写;首个读取对象必须在成功提交后取得 成功返回后框架管理租出的 owner;失败归还内部已租出的存储
TakeOwnership 默认认为调用方已完成写入,初始版本为 1 成功返回才接管 owner;抛错时不释放调用方 owner
ImportNative 默认认为 SDK 写入已结束,初始版本为 1 成功返回才接管一份已经由调用方取得的有效 SDK/原生资源引用;抛错时不调用 releaseAsync

minimumCapacity、capacity 必须大于零,native 地址必须非零,地址与容量组合不能超出进程地址空间。是否真正可访问由导入方保证。ReadOnly + Uninitialized 无法恢复为有效内容,创建时拒绝。

Capacity 是可寻址字节范围。RetainedByteLength 是持有该输入所牵连的实际分配成本,默认取 Capacity,显式值不得小于 Capacity。SDK 给出的视图只是较大分配的一部分时,由适配器报告完整保留成本,不能仅用 ROI 长度计费。框架无法推断未声明的共享分配关系。

Rent 默认使用 MemoryPool<byte>.Shared,Capacity 限于请求的可寻址长度,RetainedByteLength 按 owner 报告的完整租用成本计量,不把池额外容量暴露为有效图像。自定义 pool 的寿命由调用方管理,必须覆盖所有在途 owner 的归还;工厂不会销毁该 pool。

TakeOwnership 成功后调用方不再访问或释放原 owner;不要同时给 owner 和 buffer 套两个独立的 using。ImportNative 的回调只对应接管的那一份 SDK 引用,不能归还另一个消费者仍持有的引用。

native 初版导入要求地址全寿命稳定,并且 SDK 在这份资源引用结束前不再次写入或复用内存。只在回调内有效的指针使用图像复制工厂,不得以空释放回调伪装为长期借用。

# handle、异步等待与清理

Share() 返回新的 handle,共用 BufferId、内容版本与访问协调。每个 handle 独立 Dispose;释放一个 handle 不关闭其他 handle。ReturnState 是底层实体状态,不是当前 handle 是否已关闭。

异步权限申请在入队时保留内部存活引用。调用方随后释放发起申请的 handle,不撤销已经登记的申请;取消等待须使用令牌。所有新方法调用与 handle 的 Dispose 竞争时由单一协调点裁定,已经关闭的 handle 不得发起新申请。资源只有在全部 handle、活动访问和排队申请结束后才进入 Returning。

Dispose() 幂等,只关闭当前 handle,不阻塞 UI 或渲染线程等待其他消费者、native 工作或 SDK 回调。元数据、ReturnState 和 ReturnCompleted 仍可查询;Share/申请权限则抛 ObjectDisposedException。

releaseAsync 不接收取消令牌,恰好调用一次,其完成表示真实归还完成。框架在资源清理队列调用它,不在访问锁、UI 属性通知或 render 回调中调用;它可以将工作转发到 SDK 指定线程,并返回一个在实际归还后才完成的 ValueTask。托管宿主没有后台线程时只支持适用的非阻塞清理,不能据此假定 Browser 具备 SDK 线程能力。

SDK 导入阶段必须保证最终清理提交不会因队列已满而丢失:由有界外借数量限制清理工作,或提前预留归还席位及投递所需对象。最后一次 Dispose 只投递,不同步等待队列容量。某个供应商回调长时间未完成不得串行阻塞其他存储的归还;清理调度采用受控并发并独立观察各项结果。

ReturnCompleted 是可重复等待的 Task:归还成功完成,归还失败 fault,绝不因某个调用方停止等待而取消。失败进入 ReturnFailed,禁止恢复访问或自动重试。调用方若要等待,先关闭自己的 handle/资源引用,再等待此 Task;持有最后一份引用并等待自身归还会形成等待环。

ReturnFailed 不等于资源已经归还,适配器保留失败诊断与隔离状态,不把该槽放回可用池,也不将未知归还成本当作成功释放。供应商明确的修复或设备关闭策略属于会话故障处置,不能由核心偷偷重试 Release。

同一有效期内一份实际分配只建立一个协调实体,其他 handle 与 ROI 从它派生,不能重复包装别名来绕过互斥。buffer 与可写 image handle 仅保持存储存活;活动读取、从读取派生的稳定图像和 DisplaySnapshot 的原图读取都保留对应访问权限。稳定图像内部的持久读取也必须计入访问账本。

# 读取与写入像素

OpenReadAsync 返回 ImagePixelReader,using 结束前保持这一版像素稳定;OpenWriteAsync 返回 ImagePixelWriter,在关闭前独占缓冲。writer.Complete 表示内容已经准备好,可以再次读取;没有 Complete 就关闭不发布可能只完成一部分的内容。

public sealed class ImagePixelReader : IDisposable
{
    public long AccessId { get; }
    public ImageContentVersion Version { get; }
    public long Capacity { get; }
    public long RetainedByteLength { get; }

    public ImagePixelReader Share();
    public ImageBuffer ShareBuffer();
    public ReadOnlySpan<byte> GetSpan(long offset, int length);
    public ReadOnlySpan<byte> GetPixelRow(ImageInfo info, int row);
    public ImageBufferPin Pin();
    public void Dispose();
}

public sealed class ImagePixelWriter : IDisposable
{
    public long AccessId { get; }
    public Guid BufferId { get; }
    public long Capacity { get; }

    public Span<byte> GetSpan(long offset, int length);
    public Span<byte> GetPixelRow(ImageInfo info, int row);
    public ImageBufferPin Pin();
    public ImageContentVersion Complete();
    public ImagePixelReader CompleteAndOpenRead();
    public void Dispose();
}

public sealed class ImageBufferPin : IDisposable
{
    public nint Address { get; }
    public long Capacity { get; }
    public ImageBufferAccess Access { get; }
    public void Dispose();
}

# 访问授予

所有读写对象的 Dispose 幂等。使用与 Dispose 不得并发,唯一例外是实现须原子裁定 Share/ShareBuffer 与 Dispose 的竞争,成功返回的子对象始终独立有效。对象关闭后 GetSpan/GetPixelRow/Pin/Complete 等操作抛 ObjectDisposedException;身份、版本和容量等值元数据仍可读取。

读写对象不通过终结器代替 Dispose,也不承诺丢失托管引用后 SDK 会自动归还。没有关闭的 reader、writer、pin 或 buffer 可能阻止资源复用;清理诊断以显式打开/关闭记录为准。

# 写入提交

Complete 原子发布新版本并结束写权限,返回包含 BufferId 与 Generation 的 ImageContentVersion;CompleteAndOpenRead 在同一原子边界提交并转为读权限,返回新版本读取对象,期间不能插入另一个写者。返回对象和必要状态必须在提交前构造完成,不能内容已提交却因返回资源引用分配失败丢失访问权。两者均不允许重复调用。

没有 Complete 就 Dispose 表示写入中止,内容变为 Invalid。其字节可能已部分修改,既不回滚,也不把旧代次重新发布为有效内容。按初始化与失败恢复的完整低层恢复才能重新读取。Generation 从 1 开始,每次提交严格增加;到 long.MaxValue 时在下次授予写入前拒绝,保持原内容可读,调用方可先复制到新存储,不等到像素已改写后才发现无法发布。

框架不追踪逐字节写入完成度,尤其无法观察 native 指针上的写入;Complete 是调用方对待发布内容已完整准备的显式声明。Invalid 后的恢复完整性由写入方保证,不能把版本验证理解成内容校验或事务回滚。

# span 与 pin 的有效期

GetPixelRow(info, row) 返回指定逻辑行的有效像素,忽略行尾空隙;info 不得为 null,并使用图像模型的 ImageInfo 范围验证。GetSpan 返回分配内任意受检字节范围,不承诺是连续图像。所有返回的 span 只能在 reader/writer 有效期内使用。

Pin 返回分配基地址,ROI 起点为 Address + info.StartOffsetBytes,负步长仍按逻辑行索引推进。托管内存在 pin 期间固定,native 导入直接使用其稳定地址;两条路径都保留资源与访问权限。读取对象产生的 pin 的 Access 永远是 ReadOnly,即使底层 buffer 可写。

pin 独立保留访问有效期:父读取对象先 Dispose 时,pin 仍阻止写入和归还。父写入对象结束前必须先释放其全部 pin,Complete/CompleteAndOpenRead 在存在写 pin 时抛 InvalidOperationException;父写入对象先 Dispose 时内容中止,但独占权限持续到最后一个写 pin 释放,期间不授予新访问。

native 异步算法可以跨 await 持有 pin,但必须等待 native 工作实际结束后再释放。API 不能拦截保存在外部的地址,也不能因 native API 的取消等待已返回就推断设备或工作线程不再访问指针。pin 不提供裸 MemoryHandle 转移,禁止绕过其生命周期管理。

# 首次初始化、ROI 写入中止与恢复

未初始化图像不能发布到稳定边,也不能创建可读 ROI。类型化 Rent 的首次写入必须由根图像覆盖全部逻辑像素后 Complete;不能仅写一个 ROI 就把整图声明为有效。图像层记录这个初始根范围,未完成首次发布前拒绝登记不同范围的初始化视图。raw ImageBuffer 的高级写入路径仍由调用方对发布范围完整性负责。

正常已初始化图像允许修改 ROI;Complete 推进整份存储的 Generation,未修改区域沿用其原有有效内容。任何写入中止都使整个协调实体 Invalid,不把失败局限为一个缺少跟踪的矩形。此后类型化图像的读、写、稳定发布和 ROI 派生均拒绝;不能用另一个小 ROI 的 Complete 把整份内容重新标为有效。

常规任务流丢弃失败输出或从稳定输入重新生成。高级低层恢复只能由拥有完整初始化范围的适配器执行无期望版本的 ImageBuffer 写入,完成全部必要内容后重新发布;框架不提供字节级回滚或自动覆盖检测。带期望版本的恢复始终拒绝 Invalid 内容。

逻辑像素有效不代表行尾空隙、前缀和池额外字节已初始化。GetPixelRow、Copy、序列化默认只处理逻辑像素,不输出池中残留的 padding。GetSpan、Pin 和 Create 自定义重解释是高级能力,调用方保证其访问或发布的范围有效;仅通过容量校验不能证明任意重解释字节均已写入。需要导出完整含 padding 的内存时显式初始化相应字节。

# 三组正交状态

状态维度 内容 约束
资源归还 Active / Returning / Returned / ReturnFailed Active 只表示仍有资源,不保证像素可读
内容状态 Uninitialized / Ready / Writing / Invalid 只在 Ready 授予新读取;首次成功提交或完整恢复后回到 Ready
活动权限 活动读、独占写、pin 与排队申请 写与读互斥;共享稳定图像计入读;最后引用及工作退出才归还

写入授予时内容进入 Writing;Complete 发布 Generation 并恢复 Ready;未完成即关闭进入 Invalid。写 pin 仍在时即便内容已 Invalid,也继续占用独占权限,其他申请不能进入。Generation 记录最后一次成功提交,不能仅凭其数值非零跳过内容状态检查。所有状态转移由同一存储协调点原子裁定,回调和 continuation 在锁外执行。

单个 handle 已关闭与底层资源状态也不同:关闭一个 image 不表示其 Share、稳定发布、pin 或等待申请已经结束。稳定发布图像的关闭必须归还它内部持久读引用,不能只减生命周期计数;这是队列清空后写者和 SDK 回收能够恢复推进的必要条件。