---
name: agfx-render-targets-and-passes
description: ALWAYS use when creating agfxRenderTarget or agfxRenderPass objects in AGFX, or wiring up color/depth attachments, load/store ops, viewport/scissor, or draw calls within a render pass. Trigger for agfxRenderTargetCreate, agfxRenderPassBegin/End, agfxRenderPassAttachment, AGFX_LOAD_OPERATION_*, AGFX_STORE_OPERATION_*, agfxRenderPassSetViewport/SetScissor, agfxRenderPassDraw/DrawIndexed/DrawMesh, GBuffer/HDR/depth target setup, "wrong render target format", "render pass validation error". Do NOT trigger for swap chain acquire/present or CAMetalLayer-equivalent presentation — use agfx-presentation-and-swapchain. Do NOT trigger for fences, frames-in-flight, or barrier agglomeration semantics — use agfx-synchronization.
---

# AGFX Render Targets & Render Passes

## Overview

AGFX render targets and render passes mirror the way Metal 4, D3D12, and Vulkan dynamic rendering all express "attachment then pass", flattened behind one API. An `agfxRenderTarget` is a thin, cheap-to-create wrapper around a texture (or texture subresource) that tells the backend how to attach it — as a color or depth-stencil target, at a given mip/layer. An `agfxRenderPass` is the actual encoding scope: it groups up to 8 color attachments and one optional depth attachment, and every draw call in AGFX must happen inside one.

Render targets are intentionally lightweight: create one right before `agfxRenderPassBegin` and destroy it right after `agfxRenderPassEnd`. Don't cache them across frames — cache the underlying `agfxTexture` instead and re-wrap it each pass (see `deferred_renderer.cpp` for the GBuffer/HDR/depth targets recreated only on resize, but their `agfxRenderTarget` wrappers created per-pass).

## Ownership

**Owns:**
- `agfxRenderTargetCreateInfo` / `agfxRenderTargetCreate` / `agfxRenderTargetDestroy`
- `agfxRenderPassCreateInfo` / `agfxRenderPassBegin` / `agfxRenderPassEnd`
- Color/depth attachment wiring, `agfxLoadOp` / `agfxStoreOp`, clear colors
- Viewport/scissor state (`agfxRenderPassSetViewport`, `agfxRenderPassSetScissor`)
- Draw calls within a pass (`agfxRenderPassDraw`, `agfxRenderPassDrawIndexed`, `agfxRenderPassDrawMesh`) and push constants
- Render pipeline attachment-format matching (`colorFormats[]` / `depthFormat` in `agfxRenderPipelineCreateInfo` must match the pass it's used with)

**Doesn't own:**
- Swap chain back buffer acquisition/present, HDR toggling → `agfx-presentation-and-swapchain`
- Resource state barrier semantics, agglomeration, fences, frame pacing → `agfx-synchronization`
- Compute/copy passes (`agfxComputePass*`) — those are a separate pass type for compute dispatch and copies, not covered here
- GPU-driven replay inside a pass (`agfxRenderPassExecuteIndirectBundle`) and everything feeding it → `agfx-mdi`; the pass setup around it is ordinary and covered here, but the bundle itself, its barriers, and the `supportsIndirect` pipeline flag are not
- Mesh-shader pipeline setup behind `agfxRenderPassDrawMesh` (`meshShader`/`taskShader`, the reflected group sizes) → `agfx-writing-bindless-shaders`

## References

Read `agfx/agfx.h` before writing render-target/pass code — it's the single source of truth for every struct field and enum value in this API (search for `// Render target` and `// Render pass` section markers). `agfx_demo/deferred_renderer.cpp` and `agfx_demo/agfx_demo_main.cpp` show real per-frame usage: GBuffer, SSAO, shadow map, lighting, and backbuffer passes.

## Design Patterns

### Creating a render target from a texture

```cpp
agfxRenderTargetCreateInfo rtCreateInfo = {};
rtCreateInfo.texture = colorTexture;                 // must have AGFX_TEXTURE_USAGE_COLOR_ATTACHMENT
rtCreateInfo.format = AGFX_TEXTURE_FORMAT_UNKNOWN;    // inherit the texture's own format
rtCreateInfo.mipLevel = 0;
rtCreateInfo.arrayLayer = 0;
rtCreateInfo.isDepth = false;
agfxRenderTarget* colorRT = agfxRenderTargetCreate(device, &rtCreateInfo);

// Depth target
agfxRenderTargetCreateInfo depthRtCreateInfo = {};
depthRtCreateInfo.texture = depthTexture;             // must have AGFX_TEXTURE_USAGE_DEPTH_STENCIL_ATTACHMENT
depthRtCreateInfo.isDepth = true;
agfxRenderTarget* depthRT = agfxRenderTargetCreate(device, &depthRtCreateInfo);
```

`AGFX_TEXTURE_FORMAT_UNKNOWN` is the correct choice whenever the render target wraps a texture whose format is already fixed (e.g. a swap chain back buffer, queried via `agfxSwapChainGetFormat`) — don't hardcode the format in that case, it can silently mismatch on an HDR toggle.

### Required state before the pass begins

Every color attachment's underlying texture must already be in `AGFX_RESOURCE_STATE_RENDER_TARGET`, and every depth attachment's texture in `AGFX_RESOURCE_STATE_DEPTH_WRITE`, **before** calling `agfxRenderPassBegin`. Barrier into that state first:

```cpp
agfxCommandBufferTextureBarrier(cmd, colorTexture, AGFX_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, AGFX_RESOURCE_STATE_RENDER_TARGET, 0, 0, /*agglomerate=*/true);
```

See `agfx-synchronization` for the `agglomerate` flag's semantics (ignored on D3D12/Vulkan, load-bearing on Metal) — get this wrong and you'll get correct D3D12/Vulkan output with garbage or validation errors on Metal.

### Beginning a render pass with multiple attachments

```cpp
agfxRenderPassCreateInfo passInfo = {};
passInfo.colorAttachmentCount = 1;
passInfo.colorAttachments[0].renderTarget = colorRT;
passInfo.colorAttachments[0].loadOp = AGFX_LOAD_OPERATION_CLEAR;
passInfo.colorAttachments[0].storeOp = AGFX_STORE_OPERATION_STORE;
passInfo.colorAttachments[0].clearColor[0] = 0.05f;
passInfo.colorAttachments[0].clearColor[1] = 0.05f;
passInfo.colorAttachments[0].clearColor[2] = 0.1f;
passInfo.colorAttachments[0].clearColor[3] = 1.0f;

passInfo.hasDepthAttachment = true;
passInfo.depthAttachment.renderTarget = depthRT;
passInfo.depthAttachment.loadOp = AGFX_LOAD_OPERATION_CLEAR;
passInfo.depthAttachment.storeOp = AGFX_STORE_OPERATION_DONT_CARE; // transient depth, not read afterwards

passInfo.width = width;
passInfo.height = height;
passInfo.name = "GBuffer";

agfxRenderPass* pass = agfxRenderPassBegin(cmd, &passInfo);
agfxRenderPassSetViewport(pass, 0, 0, (float)width, (float)height, 0.0f, 1.0f);
agfxRenderPassSetScissor(pass, 0, 0, width, height);
agfxRenderPassSetPipeline(pass, pipeline);
agfxRenderPassPushConstants(pass, &pushData, sizeof(pushData));
agfxRenderPassDrawIndexed(pass, indexBuffer, indexCount, 1, 0, 0, 0);
agfxRenderPassEnd(pass);

agfxRenderTargetDestroy(device, colorRT);
agfxRenderTargetDestroy(device, depthRT);
```

### Load/store op choices

- `AGFX_LOAD_OPERATION_CLEAR` + `AGFX_STORE_OPERATION_STORE` — standard color/depth attachment that's cleared then read later (e.g. GBuffer albedo, depth used for SSAO)
- `AGFX_LOAD_OPERATION_DONT_CARE` + `AGFX_STORE_OPERATION_STORE` — every pixel will be fully overwritten this pass and the result is needed afterwards; skip the clear cost
- `AGFX_LOAD_OPERATION_LOAD` + `AGFX_STORE_OPERATION_STORE` — accumulating into an attachment across multiple passes without a full clear (e.g. compositing UI on top of tonemapped output)
- Any load op + `AGFX_STORE_OPERATION_DONT_CARE` — transient attachment discarded at pass end (e.g. an MSAA resolve source, or depth that's only used within the pass and never sampled afterward)

### Pipeline/pass format matching

`agfxRenderPipelineCreateInfo::colorFormats[]`/`colorAttachmentCount` and `depthFormat` must match the actual formats of the render pass's attachments the pipeline is bound in. When a pass renders to the swap chain back buffer and the app supports toggling HDR (see `agfx-presentation-and-swapchain`), the pipeline bound in that pass must be recreated whenever the back buffer format changes — `agfx_demo_main.cpp`'s `RecreateTonemapPipeline` on the HDR toggle is the reference pattern.

### Mesh-shading vs classic draws

A pipeline is either classic (`vertexShader`/`fragmentShader`, drawn with `agfxRenderPassDraw`/`agfxRenderPassDrawIndexed`) or mesh-shading (`meshShader`, optional `taskShader`, drawn with `agfxRenderPassDrawMesh`). Don't mix: calling `agfxRenderPassDrawMesh` with a classic pipeline bound (or vice versa) is undefined — match the draw call to how the bound pipeline was created.
