#include "fg.h"
#include "core/logger.h"
#include "fg_utils.h"
#include <array>
// each recorder declares rhi::ResourceState, each backend lowers to
// native stages/access/layouts. ideally compatible states ought to merge (no barrier),
// currently only readonly stuff does
// all writes in the pair force a transition, Idle is the fresh-resource state (not sure if Idle will be ketp)
static bool state_is_read_only(rhi::ResourceState s) {
using S = rhi::ResourceState;
switch (s) {
case S::Idle:
case S::ColorFetch:
case S::DepthFetch:
case S::TextureSample:
case S::TextureSampleNonFragment:
case S::StorageRead:
case S::VertexFetch:
case S::IndexFetch:
case S::IndirectFetch:
case S::UniformRead:
case S::TransferFrom:
case S::HostRead:
case S::AccelTrace:
case S::Display:
return true;
default:
return false;
}
}
static rhi::ImageUsage image_state_creation_usage(rhi::ResourceState s) {
using S = rhi::ResourceState;
switch (s) {
case S::ColorDraw:
case S::ColorFetch:
return rhi::ImageUsage::ColorAttachment;
case S::DepthDraw:
case S::DepthFetch:
return rhi::ImageUsage::DepthAttachment;
case S::TextureSample:
case S::TextureSampleNonFragment:
return rhi::ImageUsage::Sampled;
case S::StorageRead:
case S::StorageReadWrite:
return rhi::ImageUsage::Storage;
case S::TransferFrom:
return rhi::ImageUsage::TransferSrc;
case S::TransferTo:
return rhi::ImageUsage::TransferDst;
case S::Display:
return rhi::ImageUsage::TransferSrc;
default:
return rhi::ImageUsage::None;
}
}
static rhi::BufferUsage buffer_state_creation_usage(rhi::ResourceState s) {
using S = rhi::ResourceState;
using U = rhi::BufferUsage;
switch (s) {
case S::StorageRead:
case S::StorageReadWrite:
return U::StorageBuffer;
case S::UniformRead:
return rhi::BufferUsage::UniformBuffer;
case S::VertexFetch:
return rhi::BufferUsage::VertexBuffer;
case S::IndexFetch:
return rhi::BufferUsage::IndexBuffer;
case S::IndirectFetch:
return rhi::BufferUsage::IndirectBuffer;
case S::TransferFrom:
case S::HostRead:
return rhi::BufferUsage::CopySrc;
case S::TransferTo:
return rhi::BufferUsage::CopyDst;
case S::AccelBuild:
case S::AccelTrace:
return rhi::BufferUsage::AccelStructStorage;
default:
return rhi::BufferUsage::None;
}
}
// same-state merge policy for a repeated handle in one pass: keep the
// writable state (conservative union), read-only pairs keep the latest
static rhi::ResourceState merge_image_state(rhi::ResourceState a, rhi::ResourceState b) {
if (a == b) {
return a;
}
const bool wa = !state_is_read_only(a);
const bool wb = !state_is_read_only(b);
if (wa != wb) {
return wa ? a : b;
}
return b;
}
static rhi::ImageAspect aspect_from_format(rhi::ImageFormat fmt) {
switch (fmt) {
case rhi::ImageFormat::D32_FLOAT:
return rhi::ImageAspect::Depth;
// TODO: stencil?
default:
return rhi::ImageAspect::Color;
}
}
//==========================================================================================
static Operation merge_access(Operation a, Operation b) {
if (a == Operation::ReadWrite || b == Operation::ReadWrite) {
return Operation::ReadWrite;
}
if (a == Operation::Write || b == Operation::Write) {
return Operation::Write;
}
return Operation::Read;
}
ImageHandle FrameGraph::add_image(ImageResource img) {
assert(
(img.lifetime != Lifetime::Imported) ||
(img.external != nullptr) && "Imported resources must carry an external object (use import_image)"
);
u32 id = logical_images.size();
logical_images.push_back(img);
return {id};
}
BufferHandle FrameGraph::add_buffer(BufferResource buf) {
assert(
(buf.lifetime != Lifetime::Imported) ||
(buf.external != nullptr) && "Imported resources must carry an external object (use import_buffer)"
);
u32 id = logical_buffers.size();
logical_buffers.push_back(buf);
return {id};
}
PassBuilder &FrameGraph::add_pass(const char *name) {
passes.emplace_back();
passes.back().name = name;
passes.back().graph_ = this;
return passes.back();
}
PassBuilder &
PassBuilder::add_image_ref(FrameGraph &g, ImageHandle img, rhi::ResourceState state, Operation op, i32 version_offset) {
assert(img.is_valid() && img.id < g.logical_images.size() && "invalid image handle in PassBuilder::read");
if (image_ref_count == 0) {
image_ref_start = static_cast<u32>(g.image_refs.size());
}
for (u32 i = image_ref_start; i < image_ref_start + image_ref_count; ++i) {
ImageResourceRef &ref = g.image_refs[i];
if (ref.img.id == img.id && ref.version_offset == version_offset) {
ref.operation = merge_access(ref.operation, op);
ref.state = merge_image_state(ref.state, state);
return *this;
}
}
g.image_refs.push_back({.img = img, .operation = op, .state = state, .version_offset = version_offset});
image_ref_count++;
return *this;
}
PassBuilder &PassBuilder::add_buffer_ref(
FrameGraph &g, BufferHandle buf, rhi::ResourceState state, Operation op, i32 version_offset
) {
assert(buf.is_valid() && buf.id < g.logical_buffers.size() && "invalid buffer handle in PassBuilder::read");
if (buffer_ref_count == 0) {
buffer_ref_start = static_cast<u32>(g.buffer_refs.size());
}
for (u32 i = buffer_ref_start; i < buffer_ref_start + buffer_ref_count; ++i) {
BufferResourceRef &ref = g.buffer_refs[i];
if (ref.buf.id == buf.id && ref.version_offset == version_offset) {
ref.operation = merge_access(ref.operation, op);
ref.state = merge_image_state(ref.state, state);
return *this;
}
}
g.buffer_refs.push_back({.buf = buf, .operation = op, .state = state, .version_offset = version_offset});
buffer_ref_count++;
return *this;
}
PassBuilder &PassBuilder::read(FrameGraph &g, ImageHandle img, rhi::ResourceState state) {
return add_image_ref(g, img, state, Operation::Read, 0);
}
PassBuilder &PassBuilder::write(FrameGraph &g, ImageHandle image, rhi::ResourceState state) {
return add_image_ref(g, image, state, Operation::Write, 0);
}
PassBuilder &PassBuilder::read_write(FrameGraph &g, ImageHandle image, rhi::ResourceState state) {
return add_image_ref(g, image, state, Operation::ReadWrite, 0);
}
PassBuilder &PassBuilder::read(FrameGraph &g, BufferHandle buf, rhi::ResourceState state) {
return add_buffer_ref(g, buf, state, Operation::Read, 0);
}
PassBuilder &PassBuilder::write(FrameGraph &g, BufferHandle buffer, rhi::ResourceState state) {
return add_buffer_ref(g, buffer, state, Operation::Write, 0);
}
PassBuilder &PassBuilder::read_write(FrameGraph &g, BufferHandle buffer, rhi::ResourceState state) {
return add_buffer_ref(g, buffer, state, Operation::ReadWrite, 0);
}
// ---------------------------------------------------------------------------
// Owner-aware forms: forward to the graph this pass was recorded on.
PassBuilder &PassBuilder::read(ImageHandle img, rhi::ResourceState state) {
return read(*graph_, img, state);
}
PassBuilder &PassBuilder::write(ImageHandle image, rhi::ResourceState state) {
return write(*graph_, image, state);
}
PassBuilder &PassBuilder::read_write(ImageHandle image, rhi::ResourceState state) {
return read_write(*graph_, image, state);
}
PassBuilder &PassBuilder::read(BufferHandle buf, rhi::ResourceState state) {
return read(*graph_, buf, state);
}
PassBuilder &PassBuilder::write(BufferHandle buffer, rhi::ResourceState state) {
return write(*graph_, buffer, state);
}
PassBuilder &PassBuilder::read_write(BufferHandle buffer, rhi::ResourceState state) {
return read_write(*graph_, buffer, state);
}
// ---------------------------------------------------------------------------
// History resource versioning forms.
PassBuilder &PassBuilder::write_current(FrameGraph &g, ImageHandle img, rhi::ResourceState state) {
return add_image_ref(g, img, state, Operation::Write, 0);
}
PassBuilder &PassBuilder::read_previous(FrameGraph &g, ImageHandle img, rhi::ResourceState state) {
return add_image_ref(g, img, state, Operation::Read, -1);
}
PassBuilder &PassBuilder::write_current(FrameGraph &g, BufferHandle buf, rhi::ResourceState state) {
return add_buffer_ref(g, buf, state, Operation::Write, 0);
}
PassBuilder &PassBuilder::read_previous(FrameGraph &g, BufferHandle buf, rhi::ResourceState state) {
return add_buffer_ref(g, buf, state, Operation::Read, -1);
}
PassBuilder &PassBuilder::write_current(ImageHandle img, rhi::ResourceState state) {
return write_current(*graph_, img, state);
}
PassBuilder &PassBuilder::read_previous(ImageHandle img, rhi::ResourceState state) {
return read_previous(*graph_, img, state);
}
PassBuilder &PassBuilder::write_current(BufferHandle buf, rhi::ResourceState state) {
return write_current(*graph_, buf, state);
}
PassBuilder &PassBuilder::read_previous(BufferHandle buf, rhi::ResourceState state) {
return read_previous(*graph_, buf, state);
}
void FrameGraph::init() {
assert(
physical.images.empty() && physical.buffers.empty() &&
"call destroy() or reset() before init() to release GPU storage"
);
logical_images.clear();
logical_buffers.clear();
image_refs.clear();
buffer_refs.clear();
passes.clear();
image_barriers.clear();
buffer_barriers.clear();
extra_views.clear();
is_compiled = false;
}
static void destroy_transient_views(FrameGraph &g) {
if (g.device == nullptr) {
g.physical.transient_views.clear();
return;
}
for (rhi::ImageView &v : g.physical.transient_views) {
rhi::destroy_image_view(*g.device, v);
}
g.physical.transient_views.clear();
}
static u32 mint_transient_view(FrameGraph &g, rhi::Image *img, const rhi::ImageViewDesc &desc) {
assert(g.device != nullptr);
assert(img != nullptr);
rhi::ImageViewDesc full = desc;
full.image = img;
rhi::ImageView v{};
if (!rhi::create_image_view(*g.device, full, v)) {
VEL_CRITICAL("FrameGraph::compile: failed transient view mint");
return INVALID_VIEW_INDEX;
}
assert(v.slot <= UINT32_MAX && "bindless slot exceeds u32 shader index");
g.physical.transient_views.push_back(std::move(v));
return (u32)g.physical.transient_views.back().slot;
}
u32 FrameGraph::request_extra_view(ImageHandle img, bool all_mips) {
u32 idx = (u32)extra_views.size();
extra_views.push_back({img, all_mips, {}, 0});
return idx;
}
rhi::ImageView PassContext::image_view(ImageHandle handle, const rhi::ImageViewDesc &desc) const {
return rhi::get_cached_image_view(device, get_image(handle), desc);
}
u32 PassContext::view_extra(u32 extra_idx, i32 version_offset, u32 mip) const {
assert(extra_views != nullptr && extra_idx < (u32)extra_views->size());
const ExtraImageView &ev = (*extra_views)[extra_idx];
u32 vc = storage.image_version_count[ev.img.id];
i32 v = ((i32)frame_index + version_offset) % (i32)vc;
if (v < 0) {
v += (i32)vc;
}
if (ev.all_mips) {
assert(mip < ev.mips_per_version);
return ev.slots[(u32)v * ev.mips_per_version + mip];
}
assert(ev.slots.size() > (u32)v);
return ev.slots[(u32)v];
}
static void release_image(FrameGraph &g, rhi::Image &img) {
(void)g;
rhi::destroy_image_views(*g.device, img);
}
static void release_buffer(FrameGraph &g, rhi::Buffer &buf) {
(void)g;
rhi::destroy_buffer_views(*g.device, buf);
}
static void destroy_physicals(FrameGraph &g, bool include_persistent) {
if (g.device == nullptr) {
return;
}
destroy_transient_views(g);
for (u32 i = 0; i < g.logical_images.size(); ++i) {
if (i >= g.physical.image_base.size()) {
continue;
}
if (g.logical_images[i].lifetime == Lifetime::Imported) {
continue;
}
if (!include_persistent && (g.logical_images[i].lifetime == Lifetime::Persistent ||
g.logical_images[i].lifetime == Lifetime::History)) {
continue;
}
const u32 vc = g.physical.image_version_count[i];
for (u32 v = 0; v < vc; ++v) {
rhi::Image &img = g.physical.owned_images[g.physical.image_base[i] + v];
if (!rhi::valid(img)) {
continue;
}
// views are cached on the image; release them BEFORE destroying it
release_image(g, img);
rhi::destroy_image(*g.device, img);
}
}
for (u32 i = 0; i < g.logical_buffers.size(); ++i) {
if (i >= g.physical.buffer_base.size()) {
continue;
}
if (g.logical_buffers[i].lifetime == Lifetime::Imported) {
continue;
}
if (!include_persistent && (g.logical_buffers[i].lifetime == Lifetime::Persistent ||
g.logical_buffers[i].lifetime == Lifetime::History)) {
continue;
}
const u32 vc = g.physical.buffer_version_count[i];
for (u32 v = 0; v < vc; ++v) {
rhi::Buffer &buf = g.physical.owned_buffers[g.physical.buffer_base[i] + v];
if (!rhi::valid(buf)) {
continue;
}
release_buffer(g, buf);
rhi::destroy_buffer(*g.device, buf);
}
}
if (include_persistent) {
g.physical.owned_images.clear();
g.physical.owned_buffers.clear();
g.physical.image_base.clear();
g.physical.image_version_count.clear();
g.physical.buffer_base.clear();
g.physical.buffer_version_count.clear();
}
g.physical.images.clear();
g.physical.buffers.clear();
g.physical.image_view_index.clear();
}
static rhi::Extent2D resolve_image_extent(ImageResource &img, u32 swap_width, u32 swap_height) {
switch (img.size_class) {
case SizeOp::Absolute: {
return {img.desc.width, img.desc.height};
}
case SizeOp::Relative:
case SizeOp::Swapchain: {
u32 w = std::max(1u, static_cast<u32>(static_cast<f32>(swap_width) * img.scale.x));
u32 h = std::max(1u, static_cast<u32>(static_cast<f32>(swap_height) * img.scale.y));
return {w, h};
}
}
return {img.desc.width, img.desc.height};
}
static u32 full_mip_chain_levels(u32 w, u32 h) {
u32 levels = 1;
while ((w > 1 || h > 1) && levels < 32) {
w >>= 1;
h >>= 1;
++levels;
}
return levels;
}
static rhi::ImageDesc resolve_image_desc(ImageResource &img, rhi::ImageUsage agg_usage, u32 w, u32 h, bool readback) {
rhi::Extent2D extent = resolve_image_extent(img, w, h);
rhi::ImageDesc desc = img.desc;
desc.width = extent.width;
desc.height = extent.height;
desc.usage |= agg_usage;
if (readback) {
desc.usage |= rhi::ImageUsage::TransferSrc;
}
desc.mip_levels = desc.mip_levels == 0 ? full_mip_chain_levels(extent.width, extent.height) : desc.mip_levels;
return desc;
}
static rhi::BufferDesc resolve_buffer_desc(BufferResource &buf, rhi::BufferUsage agg_usage, u32 w, u32 h) {
rhi::BufferDesc desc = buf.desc;
if (buf.size_fn) {
desc.size = buf.size_fn(w, h);
}
desc.usage |= agg_usage;
return desc;
}
static bool same_image_desc(const rhi::ImageDesc &a, const rhi::ImageDesc &b) {
return a.width == b.width && a.height == b.height && a.depth == b.depth && a.mip_levels == b.mip_levels &&
a.layers == b.layers && a.is_cubemap == b.is_cubemap && a.sample_count == b.sample_count &&
a.format == b.format && a.usage == b.usage;
}
static bool same_buffer_desc(const rhi::BufferDesc &a, const rhi::BufferDesc &b) {
return a.size == b.size && a.usage == b.usage && a.memory == b.memory && a.dedicated == b.dedicated;
}
bool FrameGraph::compile(rhi::Device &in_device, rhi::Extent2D swapchain_extent) {
width = swapchain_extent.width;
height = swapchain_extent.height;
device = &in_device;
// clear the previous baked plan up front so a failure below leaves a clean,
// uncompiled state rather than stale barriers paired with new storage.
is_compiled = false;
pending_state_fixup = false;
image_barriers.clear();
buffer_barriers.clear();
for (PassBuilder &p : passes) {
p.image_barrier_count = 0;
p.buffer_barrier_count = 0;
}
destroy_physicals(*this, false);
// Rebuild flat-with-stride layout: base offsets + version strides.
physical.image_base.assign(logical_images.size(), 0);
physical.image_version_count.assign(logical_images.size(), 1);
u32 img_total = 0;
for (u32 i = 0; i < logical_images.size(); ++i) {
physical.image_version_count[i] = get_version_count(logical_images[i].lifetime);
physical.image_base[i] = img_total;
img_total += physical.image_version_count[i];
}
physical.buffer_base.assign(logical_buffers.size(), 0);
physical.buffer_version_count.assign(logical_buffers.size(), 1);
u32 buf_total = 0;
for (u32 i = 0; i < logical_buffers.size(); ++i) {
physical.buffer_version_count[i] = get_version_count(logical_buffers[i].lifetime);
physical.buffer_base[i] = buf_total;
buf_total += physical.buffer_version_count[i];
}
if (physical.owned_images.size() < img_total) {
physical.owned_images.resize(img_total);
} else if (physical.owned_images.size() > img_total) {
for (u32 i = img_total; i < physical.owned_images.size(); ++i) {
rhi::Image &img = physical.owned_images[i];
if (rhi::valid(img)) {
release_image(*this, img);
rhi::destroy_image(*device, img);
}
}
physical.owned_images.resize(img_total);
}
if (physical.owned_buffers.size() < buf_total) {
physical.owned_buffers.resize(buf_total);
} else if (physical.owned_buffers.size() > buf_total) {
for (u32 i = buf_total; i < physical.owned_buffers.size(); ++i) {
rhi::Buffer &buf = physical.owned_buffers[i];
if (rhi::valid(buf)) {
release_buffer(*this, buf);
rhi::destroy_buffer(*device, buf);
}
}
physical.owned_buffers.resize(buf_total);
}
std::vector<rhi::ImageUsage> img_usage(logical_images.size(), rhi::ImageUsage::None);
std::vector<rhi::BufferUsage> buffer_usage(logical_buffers.size(), rhi::BufferUsage::None);
// aggregate resource usages across all passes
for (const auto &curr_pass : passes) {
for (u32 j = 0; j < curr_pass.image_ref_count; ++j) {
const auto &r = image_refs[curr_pass.image_ref_start + j];
if (logical_images[r.img.id].lifetime != Lifetime::Imported) {
img_usage[r.img.id] |= image_state_creation_usage(r.state);
}
}
for (u32 ri = 0; ri < curr_pass.buffer_ref_count; ++ri) {
const auto &r = buffer_refs[curr_pass.buffer_ref_start + ri];
if (logical_buffers[r.buf.id].lifetime != Lifetime::Imported) {
buffer_usage[r.buf.id] |= buffer_state_creation_usage(r.state);
}
}
}
// alloc Physical Images (imports borrow survivors are reused)
physical.images.resize(img_total);
physical.image_view_index.assign(img_total, INVALID_VIEW_INDEX);
for (u32 i = 0; i < logical_images.size(); ++i) {
ImageResource &img_res = logical_images[i];
const u32 base = physical.image_base[i];
if (img_res.lifetime == Lifetime::Imported) {
if (img_res.external == nullptr) {
VEL_CRITICAL("FrameGraph::compile: imported image {} has no external image; aborting compile", i);
return false;
}
physical.images[base] = img_res.external;
continue;
}
const u32 vc = physical.image_version_count[i];
for (u32 v = 0; v < vc; ++v) {
rhi::Image &img = physical.owned_images[base + v];
if (rhi::valid(img)) {
// persistent/history survivor: reuse if its descriptor still
// matches the resolved target, otherwise recreate (e.g. resize)
rhi::ImageDesc want = resolve_image_desc(img_res, img_usage[i], width, height, image_readback);
if (same_image_desc(img.desc, want)) {
physical.images[base + v] = &img;
continue;
}
release_image(*this, img);
rhi::destroy_image(*device, img);
}
rhi::ImageDesc desc = resolve_image_desc(img_res, img_usage[i], width, height, image_readback);
if (!img_res.name.empty()) {
desc.name = img_res.name;
if (vc > 1) {
desc.name += "#v" + std::to_string(v);
}
}
if (!rhi::create_image(*device, desc, img)) {
VEL_CRITICAL("FrameGraph::compile: failed physical image allocation at index {} v{}", i, v);
physical.images[base + v] = nullptr;
continue;
}
physical.images[base + v] = &img;
}
}
for (u32 i = 0; i < logical_images.size(); ++i) {
const ImageResource &img_res = logical_images[i];
if (img_res.view.type == rhi::ImageViewType::None) {
continue;
}
const u32 base = physical.image_base[i];
const u32 vc = physical.image_version_count[i];
for (u32 v = 0; v < vc; ++v) {
rhi::Image *img = physical.images[base + v];
if (img == nullptr) {
continue;
}
rhi::ImageViewDesc desc = img_res.view;
if (desc.mip_count == 0) {
desc.mip_count = img->desc.mip_levels;
}
physical.image_view_index[base + v] = mint_transient_view(*this, img, desc);
}
}
for (ExtraImageView &ev : extra_views) {
const u32 base = physical.image_base[ev.img.id];
const u32 vc = physical.image_version_count[ev.img.id];
ev.slots.clear();
ev.mips_per_version = 0;
for (u32 v = 0; v < vc; ++v) {
rhi::Image *img = physical.images[base + v];
if (img == nullptr) {
continue;
}
if (ev.all_mips) {
u32 mips = img->desc.mip_levels;
if (ev.mips_per_version == 0) {
ev.mips_per_version = mips;
ev.slots.resize(vc * mips, INVALID_VIEW_INDEX);
}
for (u32 mip = 0; mip < mips && mip < ev.mips_per_version; ++mip) {
rhi::ImageViewDesc desc{};
desc.aspect = rhi::ImageAspect::Color;
desc.dimension = rhi::TextureViewDimension::TEXTURE_2D;
desc.type = rhi::ImageViewType::Storage;
desc.mip_start = mip;
desc.mip_count = 1;
ev.slots[v * ev.mips_per_version + mip] = mint_transient_view(*this, img, desc);
}
} else {
if (ev.slots.empty()) {
ev.slots.assign(vc, INVALID_VIEW_INDEX);
}
rhi::ImageViewDesc desc{};
desc.type = rhi::ImageViewType::Storage;
ev.slots[v] = mint_transient_view(*this, img, desc);
}
}
}
// 3. Allocate Physical Buffers (imports borrow; survivors are reused)
physical.buffers.resize(buf_total);
for (u32 i = 0; i < logical_buffers.size(); ++i) {
BufferResource &buf_res = logical_buffers[i];
const u32 base = physical.buffer_base[i];
if (buf_res.lifetime == Lifetime::Imported) {
if (buf_res.external == nullptr) {
VEL_CRITICAL("FrameGraph::compile: imported buffer {} has no external buffer; aborting compile", i);
return false;
}
physical.buffers[base] = buf_res.external;
continue;
}
const u32 vc = physical.buffer_version_count[i];
for (u32 v = 0; v < vc; ++v) {
rhi::Buffer &buf = physical.owned_buffers[base + v];
if (rhi::valid(buf)) {
// persistent/history survivor: reuse if its descriptor still
// matches the resolved target, otherwise recreate (e.g. resize)
rhi::BufferDesc want = resolve_buffer_desc(buf_res, buffer_usage[i], width, height);
if (same_buffer_desc(buf.desc, want)) {
physical.buffers[base + v] = &buf;
continue;
}
release_buffer(*this, buf);
rhi::destroy_buffer(*device, buf);
}
rhi::BufferDesc desc = resolve_buffer_desc(buf_res, buffer_usage[i], width, height);
if (!buf_res.name.empty()) {
desc.name = buf_res.name;
if (vc > 1) {
desc.name += "#v" + std::to_string(v);
}
}
if (!rhi::create_buffer(*device, desc, buf)) {
VEL_CRITICAL("FrameGraph::compile: failed physical buffer allocation at index {} v{}", i, v);
physical.buffers[base + v] = nullptr;
continue;
}
physical.buffers[base + v] = &buf;
}
}
image_barriers.clear();
buffer_barriers.clear();
auto slot_index = [](i32 version_offset) -> u32 { return (version_offset < 0) ? 1u : 0u; };
std::vector<std::array<rhi::ResourceState, 2>> sim_img(logical_images.size());
std::vector<std::array<rhi::ResourceState, 2>> sim_buf(logical_buffers.size());
for (u32 i = 0; i < logical_images.size(); ++i) {
if (logical_images[i].lifetime == Lifetime::Imported) {
sim_img[i][0] = logical_images[i].entry_state;
} else if (
(logical_images[i].lifetime == Lifetime::Persistent || logical_images[i].lifetime == Lifetime::History) &&
rhi::valid(physical.owned_images[physical.image_base[i]])
) {
sim_img[i][0] = physical.owned_images[physical.image_base[i]].state;
sim_img[i][1] = sim_img[i][0];
} else {
sim_img[i] = {};
}
}
for (u32 i = 0; i < logical_buffers.size(); ++i) {
if (logical_buffers[i].lifetime == Lifetime::Imported) {
sim_buf[i][0] = logical_buffers[i].entry_state;
} else if (
(logical_buffers[i].lifetime == Lifetime::Persistent || logical_buffers[i].lifetime == Lifetime::History) &&
rhi::valid(physical.owned_buffers[physical.buffer_base[i]])
) {
sim_buf[i][0] = physical.owned_buffers[physical.buffer_base[i]].state;
sim_buf[i][1] = sim_buf[i][0];
} else {
sim_buf[i] = {};
}
}
for (u32 lap = 0; lap < 2; ++lap) {
const bool bake = (lap == 1);
for (u32 pi = 0; pi < passes.size(); ++pi) {
PassBuilder &pass = passes[pi];
if (bake) {
pass.image_barrier_start = static_cast<u32>(image_barriers.size());
pass.buffer_barrier_start = static_cast<u32>(buffer_barriers.size());
}
for (u32 ri = 0; ri < pass.image_ref_count; ++ri) {
const ImageResourceRef &ref = image_refs[pass.image_ref_start + ri];
const u32 id = ref.img.id;
const u32 slot = slot_index(ref.version_offset);
rhi::ImageAspect aspect = rhi::ImageAspect::Color;
rhi::Image *phys0 = physical.image(id, 0);
if (phys0) {
aspect = aspect_from_format(phys0->desc.format);
}
rhi::ResourceState tgt = ref.state;
rhi::ResourceState &cur = sim_img[id][slot];
const bool read_after_read = state_is_read_only(tgt) && state_is_read_only(cur) && cur == tgt;
if (!read_after_read) {
// write-after-anything, anything-after-write, or a state change
// Idle cur == fresh entry -> legal discard transition
if (bake) {
image_barriers.push_back({
.image_id = id,
.version_offset = ref.version_offset,
.before = cur,
.after = tgt,
.aspect = aspect,
});
}
cur = tgt;
} else {
// pure same-state read-after-read: no barrier
cur = tgt;
}
}
for (u32 ri = 0; ri < pass.buffer_ref_count; ++ri) {
const BufferResourceRef &ref = buffer_refs[pass.buffer_ref_start + ri];
const u32 id = ref.buf.id;
const u32 slot = slot_index(ref.version_offset);
rhi::ResourceState tgt = ref.state;
rhi::ResourceState &cur = sim_buf[id][slot];
const bool read_after_read = state_is_read_only(tgt) && state_is_read_only(cur) && cur == tgt;
if (!read_after_read) {
if (bake) {
buffer_barriers.push_back({
.buffer_id = id,
.version_offset = ref.version_offset,
.before = cur,
.after = tgt,
});
}
cur = tgt;
} else {
cur = tgt;
}
}
if (bake) {
pass.image_barrier_count = static_cast<u32>(image_barriers.size()) - pass.image_barrier_start;
pass.buffer_barrier_count = static_cast<u32>(buffer_barriers.size()) - pass.buffer_barrier_start;
}
}
}
// The first execute() after a compile patches each physical slot's first
// baked barrier with its live state (fresh creates enter Idle, survivors
// may differ from steady state). Thereafter it replays the pure baked plan
pending_state_fixup = true;
touch_img.assign(img_total, 0);
touch_buf.assign(buf_total, 0);
is_compiled = true;
return true;
}
void FrameGraph::execute(rhi::CmdBuffer &cmd, const GraphExecInfo &info) {
if (!is_compiled) {
VEL_ERROR("FrameGraph::execute: graph is not compiled; ignoring execute");
return;
}
// on the first execute after a compile, patch each physical slot's first
// baked barrier with its live state. See pending_state_fixup in compile()
const bool fixup = pending_state_fixup;
pending_state_fixup = false;
if (fixup) {
touch_img.assign(touch_img.size(), 0);
touch_buf.assign(touch_buf.size(), 0);
}
if (timings) {
timings->reset_frame(cmd, info.frame_index);
}
for (u32 pass_index = 0; pass_index < passes.size(); ++pass_index) {
PassBuilder &pass = passes[pass_index];
for (u32 i = 0; i < pass.image_barrier_count; ++i) {
const ImageBarrierBaked &b = image_barriers[pass.image_barrier_start + i];
const u32 vc = physical.image_version_count[b.image_id];
i32 v = ((i32)info.frame_index + b.version_offset) % (i32)vc;
if (v < 0) {
v += (i32)vc;
}
rhi::Image &img = *physical.image(b.image_id, (u32)v);
const bool always_live = (vc > 1) || logical_images[b.image_id].lifetime == Lifetime::Imported;
bool first_touch = false;
if (fixup && !always_live) {
const u32 slot = physical.image_base[b.image_id] + (u32)v;
if (slot < touch_img.size() && !touch_img[slot]) {
touch_img[slot] = 1;
first_touch = true;
}
}
const rhi::ResourceState before = (always_live || first_touch) ? img.state : b.before;
rhi::ImageRange range{};
range.aspect = b.aspect;
rhi::barrier(cmd, img, range, before, b.after);
}
for (u32 i = 0; i < pass.buffer_barrier_count; ++i) {
const BufferBarrierBaked &b = buffer_barriers[pass.buffer_barrier_start + i];
const u32 vc = physical.buffer_version_count[b.buffer_id];
i32 v = ((i32)info.frame_index + b.version_offset) % (i32)vc;
if (v < 0) {
v += (i32)vc;
}
rhi::Buffer &buf = *physical.buffer(b.buffer_id, (u32)v);
const bool always_live = (vc > 1) || logical_buffers[b.buffer_id].lifetime == Lifetime::Imported;
bool first_touch = false;
if (fixup && !always_live) {
const u32 slot = physical.buffer_base[b.buffer_id] + (u32)v;
if (slot < touch_buf.size() && !touch_buf[slot]) {
touch_buf[slot] = 1;
first_touch = true;
}
}
rhi::barrier(cmd, buf, (always_live || first_touch) ? buf.state : b.before, b.after);
}
if (pass.execute_fn) {
if (timings) {
timings->begin_pass(cmd, pass_index, info.frame_index);
}
PassContext ctx{
cmd,
*device,
physical,
&extra_views,
info.frame_data,
info.render_data,
info.scene,
info.frame_index,
info.frame_count,
width,
height,
};
pass.execute_fn(ctx);
if (timings) {
timings->end_pass(cmd, pass_index, info.frame_index);
}
}
}
}
void FrameGraph::destroy() {
destroy_physicals(*this, true);
init();
}
void FrameGraph::reset() {
destroy_physicals(*this, true);
init();
}
ImageHandle FrameGraph::import_image(rhi::Image *img, rhi::ResourceState entry_state, const rhi::ImageViewDesc &view) {
assert(img && "import_image requires a valid external image");
ImageResource res;
res.desc = img->desc;
res.lifetime = Lifetime::Imported;
res.size_class = SizeOp::Absolute;
res.external = img;
res.entry_state = entry_state;
res.view = view;
return add_image(res);
}
BufferHandle FrameGraph::import_buffer(rhi::Buffer *buf, rhi::ResourceState entry_state) {
assert(buf && "import_buffer requires a valid external buffer");
BufferResource res;
res.desc = buf->desc;
res.lifetime = Lifetime::Imported;
res.size_class = SizeOp::Absolute;
res.external = buf;
res.entry_state = entry_state;
return add_buffer(res);
}
void FrameGraph::overwrite_imported_image(ImageHandle h, rhi::Image *img) {
assert(h.is_valid() && h.id < logical_images.size() && "invalid imported image handle");
assert(logical_images[h.id].lifetime == Lifetime::Imported && "only imported images can be overwritten");
assert(img != nullptr && "overwrite_imported_image requires a valid image");
logical_images[h.id].external = img;
u32 base = physical.image_base[h.id];
if (base < physical.images.size()) {
physical.images[base] = img;
}
}