Files
agent_compositor_test/.opencode/skills/prism-rhi/SKILL.md
T
2026-07-12 13:43:59 +01:00

5.6 KiB

name, description, license, compatibility, metadata
name description license compatibility metadata
prism-rhi RHI (Rendering Hardware Interface) patterns — compile-time dispatch, by-value desc structs, wapp array aliases, backend file layout MIT opencode
domain
rendering

What I do

Captures the RHI conventions for Prism: how backends are dispatched, how descriptor structs and arrays are handled, and how the files are organized.

When to use me

Use this when working on any file in src/prism/rhi/, or when creating a new backend (Vulkan, D3D12, Metal).

Conventions

Global context

RHI functions do not take allocator parameters. A global PrRhiContext provides two allocators:

  • allocator — for user-facing objects (buffers, textures, pipelines, etc.)
  • tmp — for short-lived internal temporaries
extern PrRhiContext _G_RHI_CONTEXT;

void prRhiInit(void);   // sets up both allocators
void prRhiDestroy(void); // tears down context

All RHI functions access _G_RHI_CONTEXT directly. Do not pass allocators to RHI API calls.

Backend dispatch

Backend selection is compile-time via -D PR_RHI_VULKAN / -D PR_RHI_D3D12 / -D PR_RHI_METAL. The umbrella header pr_rhi.h includes the appropriate alias file:

#if defined(PR_RHI_VULKAN)
#  include "vulkan/pr_rhi_vk_aliases.h"
#elif defined(PR_RHI_D3D12)
#  include "d3d12/pr_rhi_d3d12_aliases.h"
#elif defined(PR_RHI_METAL)
#  include "metal/pr_rhi_metal_aliases.h"
#else
#  error "Define one of: PR_RHI_VULKAN, PR_RHI_D3D12, PR_RHI_METAL"
#endif

Each _aliases.h file maps generic names to backend-specific names:

#define prRhiCreateDevice       prRhiCreateDeviceVk
#define prRhiCreateSwapchain    prRhiCreateSwapchainVk
#define prRhiCreateBuffer       prRhiCreateBufferVk
// …

Backend implementations are suffixed with the backend name: pr_rhi_vk_device.c, pr_rhi_vk_swapchain.c, etc.

Desc structs

All descriptor structs are passed by value, not const *:

// correct
PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, PrRhiDeviceDesc desc);

// wrong
PrRhiDevice *prRhiCreateDevice(PrRhiPhysicalDevice *pdev, PrRhiSurface *surface, const PrRhiDeviceDesc *desc);

Frame-by-frame command batching

Commands that run every frame must avoid arena allocation. Use stack arrays with a while-loop to batch operations:

// correct — stack array, batched submission
void prRhiCmdBindDescriptorSetsVk(PrRhiCommandBuffer *cb, PrRhiPipelineBindPoint bind_point,
                                  PrRhiPipelineLayout *layout, u32 first_set,
                                  PrRhiDescriptorSetArray sets) {
	u32 set_count = sets ? (u32)wpArrayCount(sets) : 0;
	while (set_count > 0) {
		VkDescriptorSetArray vk_sets = wpArrayWithCapacity(VkDescriptorSet, 16, WP_ARRAY_INIT_FILLED);
		u32 total_capacity = (u32)wpArrayCapacity(vk_sets);
		u32 real_count = set_count < total_capacity ? set_count : total_capacity;
		for (u32 i = 0; i < real_count; ++i) {
			vk_sets[i] = sets[i]->handle;
		}
		vkCmdBindDescriptorSets(cb->handle, vk_bp, vk_layout, first_set, real_count, vk_sets, 0, NULL);
		set_count -= real_count;
		first_set += real_count;
	}
}

// wrong — allocates from arena on every call
void prRhiCmdBindDescriptorSetsBad(PrRhiCommandBuffer *cb, ...) {
	VkDescriptorSetArray vk_sets = wpArrayAllocCapacity(VkDescriptorSet, &_G_RHI_CONTEXT.allocator, count, ...);
	// ... this leaks every frame
}

Apply this pattern to: prRhiCmdBindDescriptorSets, prRhiCmdBindVertexBuffers, prRhiCmdCopyBufferToImage, and any other command that processes user-provided arrays.

Opaque struct handles — no casts

Handle types in opaque structs are already the correct Vulkan type. Do not cast:

// correct
vk_device = device->handle;
vk_buffer = buffer->handle;

// wrong
vk_device = (VkDevice)device->handle;
vk_buffer = (VkBuffer)buffer->handle;

Vulkan struct initialisation — designated initializers

Always use C99 designated initializers for Vulkan info structs:

// correct
VkBufferCreateInfo buf_info = {
    .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
    .size  = desc.size,
    .usage = _toVkBufferUsage(desc.usage),
};

// wrong
VkBufferCreateInfo buf_info = {};
buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buf_info.size  = desc.size;
buf_info.usage = _toVkBufferUsage(desc.usage);

API patterns

  • prRhiCreateCommandPool: Takes only PrRhiDevice *device (uses device->queue_family_index internally)
  • prRhiFreeCommandBuffers: Takes PrRhiCommandBufferArray buffers (count derived from wpArrayCount)
  • prRhiAllocateDescriptorSet: Takes WpU32Array variable_descriptor_counts for variable descriptor support
  • prRhiCmdBindVertexBuffers: Takes WpU64Array offsets (count matched to buffers internally)
  • Shader entry points: Configurable via vertex_shader_entry_point / fragment_shader_entry_point in pipeline desc (not hardcoded to "main")

File layout

src/prism/rhi/
├── pr_rhi.h              ← umbrella header (API declarations + backend dispatch)
├── pr_rhi.c              ← global context definition (prRhiInit, prRhiDestroy)
├── pr_rhi_types.h        ← shared types (enums, element types, array aliases, desc structs, opaque handles)
├── vulkan/
│   ├── pr_rhi_vk.h       ← Vulkan backend header (opaque struct defs + Vk-suffixed decls)
│   ├── pr_rhi_vk.c       ← Vulkan backend implementation
│   ├── pr_rhi_vk_aliases.h ← #define alias mapping
│   └── profiles/          ← generated Vulkan Profiles library
├── d3d12/
│   └── …
└── metal/
    └── …