cmd.cpp

cross platform rendering playground

src/backend/metal/cmd.cpp

14.83 KB
#include <math.h>
#include <new>
#ifndef INFINITY
#define INFINITY __builtin_huge_valf()
#endif
#include <Foundation/Foundation.hpp>
#include <Metal/Metal.hpp>
#include <QuartzCore/QuartzCore.hpp>

#include "backend/metal/mt_api.h"
#include "backend/metal/mt_internal.h"
#include "core/logger.h"

namespace mt {

static CA::MetalDrawable *g_current_drawable = nullptr;
static MTL4::CommandBuffer *g_last_committed = nullptr;

void mt_set_current_drawable(CA::MetalDrawable *drawable) {
    if (g_current_drawable) {
        g_current_drawable->release();
    }
    g_current_drawable = drawable;
    if (g_current_drawable) {
        g_current_drawable->retain();
    }
}

CA::MetalDrawable *mt_current_drawable() {
    return g_current_drawable;
}

void mt_clear_current_drawable() {
    if (g_current_drawable) {
        g_current_drawable->release();
        g_current_drawable = nullptr;
    }
}

void mt_wait_idle() {
    (void)g_last_committed;
}

} // namespace mt

namespace rhi {
using namespace mt;

bool create_cmd_pool(Device &device, const rhi::CmdPoolDesc &desc, CmdPool &pool) {
    (void)device;
    (void)desc;
    // no pools on metal
    pool.handle = (void *)1;
    return true;
}

void destroy_cmd_pool(Device &device, CmdPool &pool) {
    (void)device;
    pool.handle = nullptr;
}

void reset_cmd_pool(Device &device, CmdPool &pool, u32 flags) {
    (void)device;
    (void)pool;
    (void)flags;
}

bool create_cmd_buffer(Device &device, const CmdBufferDesc &desc, CmdBuffer &cmd) {
    (void)desc;
    cmd.queue = device.graphics.handle;
    if (!cmd.queue || !device.heap) {
        return false;
    }
    cmd.table = device.heap->table;
    cmd.residency = device.heap->residency;
    cmd.allocator = cmd.queue->device()->newCommandAllocator();
    return cmd.table && cmd.residency && cmd.allocator;
}

void destroy_cmd_buffer(Device &device, CmdPool &pool, CmdBuffer &cmd) {
    (void)device;
    (void)pool;
    for (MTL::Buffer *t : cmd.transients) {
        if (t) {
            t->release();
        }
    }
    cmd.transients.clear();
    if (cmd.handle) {
        cmd.handle->release();
        cmd.handle = nullptr;
    }
    if (cmd.allocator) {
        cmd.allocator->release();
        cmd.allocator = nullptr;
    }
    cmd.encoder = nullptr;
    cmd.queue = nullptr;
    cmd.table = nullptr;
    cmd.residency = nullptr;
    cmd.is_rendering = false;
}

bool begin_cmd_buffer(CmdBuffer &cmd, const rhi::CmdBufferBeginDesc &desc) {
    (void)desc;
    for (MTL::Buffer *t : cmd.transients) {
        if (t) {
            t->release();
        }
    }
    cmd.transients.clear();
    if (cmd.handle) {
        cmd.handle->release();
        cmd.handle = nullptr;
    }
    if (!cmd.queue || !cmd.allocator || !cmd.residency) {
        return false;
    }
    cmd.allocator->reset();
    MTL::Device *dev = cmd.queue->device();
    cmd.handle = dev->newCommandBuffer();
    if (!cmd.handle) {
        return false;
    }
    cmd.handle->beginCommandBuffer(cmd.allocator);
    cmd.handle->useResidencySet(cmd.residency);
    return true;
}

bool end_cmd_buffer(CmdBuffer &cmd) {
    if (cmd.handle) {
        cmd.handle->endCommandBuffer();
    }
    return true;
}

bool reset_cmd_buffer(CmdBuffer &cmd) {
    (void)cmd;
    return true;
}

void begin_rendering(CmdBuffer &cmd, RenderingInfo &ri) {
    MTL4::RenderPassDescriptor *rpd = MTL4::RenderPassDescriptor::alloc()->init();
    MTL::RenderPassColorAttachmentDescriptor *att = rpd->colorAttachments()->object(0);
    att->setTexture(mt_current_drawable()->texture());
    att->setLoadAction(MTL::LoadActionClear);
    att->setStoreAction(MTL::StoreActionStore);

    if (ri.colorAttachmentCount > 0) {
        auto &ca = ri.colorAttachments[0];
        if (ca.loadOp == rhi::LoadOp::LOAD) {
            att->setLoadAction(MTL::LoadActionLoad);
        } else {
            att->setLoadAction(MTL::LoadActionClear);
            att->setClearColor(
                MTL::ClearColor(
                    ca.clearValue.color.float32[0],
                    ca.clearValue.color.float32[1],
                    ca.clearValue.color.float32[2],
                    ca.clearValue.color.float32[3]
                )
            );
        }
    }
    rpd->setRenderTargetWidth(ri.width);
    rpd->setRenderTargetHeight(ri.height);

    // depth opt-in. pipeline must match pass.
    if (ri.depthAttachment.img && ri.depthAttachment.img->handle) {
        MTL::RenderPassDepthAttachmentDescriptor *datt = rpd->depthAttachment();
        datt->setTexture(ri.depthAttachment.img->handle);
        if (ri.depthAttachment.loadOp == rhi::LoadOp::LOAD) {
            datt->setLoadAction(MTL::LoadActionLoad);
        } else {
            datt->setLoadAction(MTL::LoadActionClear);
            datt->setClearDepth(ri.depthAttachment.clearValue.depthStencil.depth);
        }
        datt->setStoreAction(
            ri.depthAttachment.storeOp == rhi::StoreOp::STORE ? MTL::StoreActionStore : MTL::StoreActionDontCare
        );
    }

    cmd.encoder = cmd.handle->renderCommandEncoder(rpd);
    // retain. outlives pool.
    cmd.encoder->retain();
    cmd.is_rendering = true;

    rpd->release();
}

void end_rendering(CmdBuffer &cmd) {
    if (!cmd.encoder) {
        return;
    }
    cmd.encoder->endEncoding();
    cmd.encoder->release();
    cmd.encoder = nullptr;
    cmd.is_rendering = false;
}

static void submit_cmds(const CmdBuffer *cmds, u32 cmd_count) {
    for (u32 i = 0; i < cmd_count; ++i) {
        const CmdBuffer &c = cmds[i];
        MTL4::CommandBuffer *cb = c.handle;
        if (!cb || !c.queue) {
            continue;
        }
        MTL4::CommandBuffer *arr[1] = {cb};
        c.queue->commit(arr, 1);
        if (g_current_drawable) {
            c.queue->signalDrawable(g_current_drawable);
            g_current_drawable->present();
        }
        if (g_last_committed) {
            g_last_committed->release();
        }
        g_last_committed = cb;
        g_last_committed->retain();
    }
}

void queue_submit(Device &device, Queue queue, const QueueSubmitDesc &desc) {
    (void)queue;
    // no waits/signals
    (void)desc.waits;
    (void)desc.signals;

    // page in late adds before commit
    if (device.heap && device.heap->residency) {
        device.heap->residency->requestResidency();
        device.heap->residency->commit();
    }

    submit_cmds(desc.cmds, desc.cmd_count);

    mt_clear_current_drawable();
}

Sync *sync_create(Device &device, u64 initial_value, const char *name) {
    (void)name; // Metal has no timeline-semaphore naming hook here; kept for API parity.
    (void)device;
    (void)initial_value;
    Sync *sync = new (std::nothrow) Sync{};
    if (sync == nullptr) {
        return nullptr;
    }
    sync->timeline = (void *)1;
    return sync;
}

void sync_destroy(Sync *sync) {
    delete sync;
}

u64 sync_get_completed_value(Sync *sync) {
    if (sync == nullptr || sync->timeline == nullptr) {
        return 0;
    }
    return UINT64_MAX;
}

bool sync_host_wait(Sync *sync, u64 value, u64 timeout_ns) {
    (void)value;
    (void)timeout_ns;
    if (sync == nullptr || sync->timeline == nullptr) {
        return false;
    }
    return true;
}

void set_pipeline(CmdBuffer &cmd, RenderPipeline &pipeline) {
    if (cmd.encoder && pipeline.handle && cmd.table) {
        cmd.encoder->setRenderPipelineState(pipeline.handle);
        if (pipeline.depth_stencil) {
            cmd.encoder->setDepthStencilState(pipeline.depth_stencil);
        }
        cmd.encoder->setArgumentTable(cmd.table, MTL::RenderStageVertex | MTL::RenderStageFragment);
    }
}

void set_pipeline(CmdBuffer &cmd, ComputePipeline &pipeline) {
    (void)cmd;
    (void)pipeline;
    VEL_ERROR("set_pipeline(compute) is not implemented on Metal");
}

void destroy_pipeline(Device &device, RenderPipeline &p) {
    (void)device;
    p.const_blocks.clear();
    if (p.depth_stencil) {
        p.depth_stencil->release();
        p.depth_stencil = nullptr;
    }
    if (p.handle) {
        p.handle->release();
        p.handle = nullptr;
    }
}

void destroy_pipeline(Device &device, ComputePipeline &p) {
    (void)device;
    (void)p;
    VEL_ERROR("destroy_pipeline(compute) is not implemented on Metal");
}

void set_vertex_buffer(CmdBuffer &cmd, Buffer &buffer, u32 binding, u64 offset) {
    // binding b -> index 2+b. matches pipeline.
    (void)offset;
    if (!cmd.encoder || !buffer.handle || !cmd.table) {
        return;
    }
    if (binding >= kMaxVertexBindings) {
        VEL_ERROR("set_vertex_buffer: binding {} exceeds max {}", binding, kMaxVertexBindings);
        return;
    }
    cmd.table->setAddress(buffer.handle->gpuAddress(), kVertexBaseIndex + binding);
}

void set_index_buffer(CmdBuffer &cmd, Buffer &buffer, u32 offset, IndexType type) {
    cmd.index_buffer = buffer.handle;
    cmd.index_type = type;
    cmd.index_offset = offset;
}

void set_constants(CmdBuffer &cmd, RenderPipeline &pipeline, rhi::ShaderStage stage, u32 size, const void *data) {
    (void)stage;
    if (!cmd.encoder || !cmd.table || size == 0 || !data) {
        return;
    }
    u32 table_index = 0;
    if (!pipeline.const_blocks.empty()) {
        u32 matches = 0;
        for (auto &b : pipeline.const_blocks) {
            if (b.byte_size == size) {
                table_index = b.table_index;
                ++matches;
            }
        }
        if (matches != 1) {
            VEL_ERROR("set_constants: size {} matches {} constant blocks (need exactly 1)", size, matches);
            return;
        }
    }
    MTL::Buffer *tmp = mt::push_bump_alloc(cmd, data, size);
    if (tmp) {
        cmd.table->setAddress(tmp->gpuAddress(), table_index);
    }
}

void draw(CmdBuffer &cmd, u32 vertexCount, u32 instanceCount, u32 firstVertex, u32 firstInstance) {
    if (cmd.encoder) {
        cmd.encoder->drawPrimitives(MTL::PrimitiveTypeTriangle, firstVertex, vertexCount, instanceCount, firstInstance);
    }
}

void draw_indexed(CmdBuffer &cmd, u32 indexCount, u32 instanceCount, u32 firstIndex, u32 vtxOff, u32 firstInstance) {
    if (!cmd.encoder) {
        return;
    }
    if (!cmd.index_buffer) {
        VEL_ERROR("draw_indexed: no index buffer bound (call set_index_buffer first)");
        return;
    }
    MTL::IndexType type = (cmd.index_type == IndexType::UINT16) ? MTL::IndexTypeUInt16 : MTL::IndexTypeUInt32;
    NS::UInteger index_size = (cmd.index_type == IndexType::UINT16) ? sizeof(u16) : sizeof(u32);
    MTL::GPUAddress addr = cmd.index_buffer->gpuAddress();
    addr += cmd.index_offset + (u64)firstIndex * index_size;
    cmd.encoder->drawIndexedPrimitives(
        MTL::PrimitiveTypeTriangle,
        indexCount,
        type,
        addr,
        (NS::UInteger)indexCount * index_size,
        instanceCount,
        (NS::Integer)vtxOff,
        firstInstance
    );
}

void set_viewports(CmdBuffer &cmd, Viewport *viewports, u32 viewportCount) {
    if (!cmd.encoder || !viewports || viewportCount == 0) {
        return;
    }
    const Viewport &vp = viewports[0];
    MTL::Viewport mvp;
    mvp.originX = vp.x;
    mvp.originY = vp.y;
    mvp.width = vp.width;
    mvp.height = vp.height;
    mvp.znear = vp.depth_min;
    mvp.zfar = vp.depth_max;
    cmd.encoder->setViewport(mvp);
}

void set_scissors(CmdBuffer &cmd, const Rect *rects, u32 rectCount) {
    if (!cmd.encoder || !rects || rectCount == 0) {
        return;
    }
    const Rect &r = rects[0];
    MTL::ScissorRect sr;
    sr.x = r.x > 0 ? (NS::UInteger)r.x : 0;
    sr.y = r.y > 0 ? (NS::UInteger)r.y : 0;
    sr.width = r.width > 0 ? (NS::UInteger)r.width : 0;
    sr.height = r.height > 0 ? (NS::UInteger)r.height : 0;
    cmd.encoder->setScissorRect(sr);
}

bool valid(const CmdBuffer &cmd) {
    return cmd.handle != nullptr;
}

bool valid(const CmdPool &pool) {
    return pool.handle != nullptr;
}

bool valid(const Swapchain &swapchain) {
    return swapchain.layer != nullptr;
}

bool valid(const Sync *sync) {
    return sync != nullptr && sync->timeline != nullptr;
}

bool valid(const Buffer &buffer) {
    return buffer.handle != nullptr;
}

bool valid(const RenderPipeline &pipeline) {
    return pipeline.handle != nullptr;
}

bool valid(const ComputePipeline &pipeline) {
    return pipeline.handle != nullptr;
}

void set_constants(CmdBuffer &cmd, ComputePipeline &pipeline, rhi::ShaderStage stage, u32 size, const void *data) {
    (void)cmd;
    (void)pipeline;
    (void)stage;
    (void)size;
    (void)data;
    VEL_ERROR("set_constants(compute) is not implemented on Metal");
}

void barrier(CmdBuffer &cmd, ResourceState before, ResourceState after) {
    (void)cmd;
    (void)before;
    (void)after;
}

void barrier(CmdBuffer &cmd, Buffer &buffer, ResourceState before, ResourceState after) {
    (void)cmd;
    (void)buffer;
    (void)before;
    (void)after;
}

void barrier(CmdBuffer &cmd, Image &image, const ImageRange &range, ResourceState before, ResourceState after) {
    (void)cmd;
    (void)image;
    (void)range;
    (void)before;
    (void)after;
}

void blit_image(CmdBuffer &cmd, Image &image, const ImageBlit *regions, u32 region_count) {
    (void)cmd;
    (void)image;
    (void)regions;
    (void)region_count;
    VEL_ERROR("blit_image is not implemented on Metal");
}

void copy_buffer_to_texture(
    CmdBuffer &cmd, Buffer &srcBuffer, ImageView &dstView, const BufferTextureCopyRegion &region
) {
    (void)cmd;
    if (!srcBuffer.handle || !dstView.desc.image || !dstView.desc.image->handle) {
        VEL_ERROR("copy_buffer_to_texture: null source buffer or destination image");
        return;
    }
    if (region.mip_level != 0 || region.base_array_layer != 0 || region.layer_count != 1 ||
        region.texture_offset_z != 0 || region.texture_extent_depth != 1) {
        VEL_ERROR("copy_buffer_to_texture: only mip-0/layer-0/depth-1 copies are supported on Metal");
        return;
    }
    MTL::Texture *tex = dstView.desc.image->handle;
    u32 bytes_per_pixel = 0;
    switch (tex->pixelFormat()) {
    case MTL::PixelFormatR8Unorm:
        bytes_per_pixel = 1;
        break;
    case MTL::PixelFormatRGBA8Unorm:
    case MTL::PixelFormatRGBA8Unorm_sRGB:
    case MTL::PixelFormatBGRA8Unorm:
    case MTL::PixelFormatR32Float:
        bytes_per_pixel = 4;
        break;
    case MTL::PixelFormatRGBA16Float:
        bytes_per_pixel = 8;
        break;
    default:
        break;
    }
    if (bytes_per_pixel == 0) {
        VEL_ERROR("copy_buffer_to_texture: unsupported Metal pixel format for upload");
        return;
    }
    const u8 *src = (const u8 *)srcBuffer.handle->contents();
    if (!src) {
        VEL_ERROR("copy_buffer_to_texture: staging buffer is not CPU-visible");
        return;
    }
    src += region.buffer_offset;
    u32 row_pixels = region.buffer_row_length != 0 ? region.buffer_row_length : region.texture_extent_width;
    NS::UInteger src_bytes_per_row = (NS::UInteger)row_pixels * bytes_per_pixel;
    MTL::Region dst = MTL::Region(
        region.texture_offset_x, region.texture_offset_y, region.texture_extent_width, region.texture_extent_height
    );
    tex->replaceRegion(dst, 0, src, src_bytes_per_row);
}

} // namespace rhi