Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions Examples/RenderingExtensions/ApplicationLocal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Model-Surface Argument Buffer Sample

This sample is a complete Rendering Extension that draws selected model
entities with a per-entity tint. It demonstrates an extension-owned shader
library, argument layout, model-surface pipeline, staged pass, and argument
encoding.

This is an application-local example, not a requirement that extensions live in
the application target. The same `RenderExtension` implementation can live in a
framework or Swift package. For a complete distributable package with a plugin
manifest, bundled metallib, and public installation entry point, see the
[`SwiftPackagePlugin`](../SwiftPackagePlugin/README.md) fixture.

Application-local extensions and package plugins can be active together. Use
globally unique extension and artifact IDs, register both before renderer
creation, and do not attempt to share owner-private resources between them.

## Mental Model

A Rendering Extension declares what it owns, then adds one or more passes to
the render graph. Resource declarations are recorded immediately and allocated
when Metal and any required viewport are available. Shader and pipeline hooks
are materialized when Metal is ready. The engine invokes `buildGraph` when
constructing the graph and executes the pass closures during rendering.

Only `id` and `buildGraph` are required by `RenderExtension`. The other hooks
have default no-op implementations; implement only the capabilities the
extension uses.

| Extension member | When to implement it |
| --- | --- |
| `id` | Always. Use a globally unique, stable owner ID. |
| `registerShaderLibraries` | When shaders come from an application, framework, package, or custom metallib. |
| `registerArgumentBuffers` | When a model-surface fragment shader reads extension textures, samplers, or buffers. |
| `registerPipelines` | When the extension uses a custom render pipeline. |
| `registerComputePipelines` | When the extension dispatches compute work. |
| `registerResources` | When the extension owns render textures or buffers. |
| `buildGraph` | Always. Add the passes that perform the extension's work and declare every extension resource they access. |

For a model-surface extension, implement it in this order:

1. Define an opt-in component and any per-entity settings.
2. Write a fragment shader using `UntoldModelSurfaceExtensionArguments`.
3. Choose namespaced IDs for the extension, library, pipeline, layout, and pass.
4. Register the shader library, argument layout, and model-surface pipeline.
5. Add a graph pass, declare any extension resource usage, and call
`drawModelSurfaceEntities` from its pass closure.
6. Bind per-entity values through `bindArguments`, using IDs that match Metal.
7. Register the extension before renderer creation and attach its component to
each entity it should draw.

This tint sample does not declare pass resources because it uploads value bytes
and uses engine-owned model targets. A pass that reads an extension texture or
buffer must include it in `builder.addPass(..., resources:)`; undeclared lookups
return `nil`.

The sample's data flow is:

```text
TintSurfaceComponent.color
-> bindArguments / buffer0
-> UntoldModelSurfaceExtensionArguments.buffer0
-> tintSurfaceFragment
```

## Xcode Application Target

Add `TintSurface.metal` to the application's Compile Sources build phase. Xcode
will include it in the application's default Metal library. The default
initializer loads that library from `Bundle.main`:

```swift
setRendering(.extensions(.register(TintSurfaceRenderExtension())))
```

For a framework target, pass that framework's bundle instead:

```swift
TintSurfaceRenderExtension(
shaderBundle: Bundle(for: TintSurfaceRenderExtension.self)
)
```

## Swift Package Target

Swift Package Manager copies Metal source resources without compiling them, so
compile the shader for each deployment SDK and place the result beside
`TintSurface.metal`. For macOS:

```sh
xcrun -sdk macosx metal -c \
-I <UntoldEngine>/Sources/UntoldEngineShaderSupport/include \
Shaders/TintSurface.metal -o Shaders/TintSurface.air
xcrun -sdk macosx metallib \
Shaders/TintSurface.air -o Shaders/TintSurface.metallib
```

Copy the shader directory as a resource:

```swift
.target(
name: "TintSurfaceExtension",
dependencies: [
.product(name: "UntoldEngine", package: "UntoldEngine"),
.product(name: "UntoldEngineShaderSupport", package: "UntoldEngine"),
],
resources: [
.copy("Shaders"),
]
)
```

Use the corresponding SDK and output library when building for iOS or visionOS.
The shader-support product supplies the shared ABI header; the explicit include
path makes it available to command-line Metal compilation.

Pass the package-only `Bundle.module` from code in that package target. The
engine resolves the metallib relative to that bundle:

```swift
let tintExtension = TintSurfaceRenderExtension(
shaderBundle: .module,
metallibResource: "TintSurface",
metallibSubdirectory: "Shaders"
)
setRendering(.extensions(.register(tintExtension)))
```

Register the extension before renderer creation, then attach its component to
each entity that should use the pass:

```swift
let model = createEntity()
setEntityMesh(entityId: model, filename: "sample", withExtension: "untold")
registerComponent(entityId: model, componentType: TintSurfaceComponent.self)
getEntityComponent(
entityId: model,
componentType: TintSurfaceComponent.self
)?.color = SIMD4<Float>(0.15, 0.65, 1.0, 0.8)
```

The extension uses `buffer0`, but that ID is local to its argument buffer.
Another extension can independently use `buffer0`; each draw encodes and binds
a different argument buffer at the engine-owned outer slot.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include <metal_stdlib>
#include <UntoldEngineShaderSupport/UntoldModelSurface.h>

using namespace metal;

struct TintSurfaceUniforms {
float4 color;
};

fragment float4 tintSurfaceFragment(
UntoldModelSurfaceVertexOut in [[stage_in]],
constant UntoldModelSurfaceExtensionArguments &arguments
[[buffer(UntoldModelSurfaceExtensionArgumentBufferIndex)]]
) {
constant TintSurfaceUniforms &uniforms =
*reinterpret_cast<constant TintSurfaceUniforms *>(arguments.buffer0);
return float4(
uniforms.color.rgb * uniforms.color.a,
uniforms.color.a
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import Foundation
import simd
import UntoldEngine

/// Opt-in component that marks an entity for the tint surface pass.
///
/// A model is drawn by this extension only after this component is attached to
/// the entity. Extension-specific settings also belong on this component so
/// they can be encoded separately for each entity.
public final class TintSurfaceComponent: Component {
/// Source tint whose RGB output is premultiplied by the fragment shader.
public var color = SIMD4<Float>(1.0, 1.0, 1.0, 1.0)

/// Components require a default initializer so the ECS can create them.
public required init() {}
}

/// Complete model-surface Rendering Extension using one argument-buffer value.
///
/// The implementation has four responsibilities:
/// 1. Register the shader library that owns the custom fragment function.
/// 2. Declare the argument IDs shared by Swift and Metal.
/// 3. Create a model-surface pipeline using that shader and layout.
/// 4. Add a graph pass that selects entities and encodes per-entity arguments.
public final class TintSurfaceRenderExtension: RenderExtension, @unchecked Sendable {
/// Stable owner ID used by the engine to register and remove this extension.
public let id = "sample.tintSurface"

// Registry IDs are global, so namespace every ID owned by the extension.
private let shaderLibraryID: RenderShaderLibraryID = "sample.tintSurface.shaders"
private let pipelineID: RenderPipelineType = "sample.tintSurface.pipeline"
private let argumentLayoutID = "sample.tintSurface.arguments"
private let passID = "sample.tintSurface.draw"
private let shaderBundle: Bundle
private let metallibResource: String?
private let metallibSubdirectory: String?

/// Creates an extension that loads a bundle's default Metal library.
///
/// Use the default for an Xcode application target. A framework should pass
/// its framework bundle explicitly.
public init(shaderBundle: Bundle = .main) {
self.shaderBundle = shaderBundle
metallibResource = nil
metallibSubdirectory = nil
}

/// Creates an extension that loads a bundled precompiled Metal library.
///
/// Use this initializer when a Swift package bundles a `.metallib` resource.
public init(
shaderBundle: Bundle,
metallibResource: String,
metallibSubdirectory: String? = nil
) {
self.shaderBundle = shaderBundle
self.metallibResource = metallibResource
self.metallibSubdirectory = metallibSubdirectory
}

/// Registers the library containing `tintSurfaceFragment`.
///
/// This hook is optional for extensions that use only engine shaders. This
/// sample implements it because its fragment function is extension-owned.
public func registerShaderLibraries(_ registry: RenderShaderLibraryRegistry) {
if let metallibResource {
registry.registerLibrary(
shaderLibraryID,
bundle: shaderBundle,
resource: metallibResource,
subdirectory: metallibSubdirectory
)
} else {
registry.registerDefaultLibrary(shaderLibraryID, bundle: shaderBundle)
}
}

/// Declares the local argument IDs used by the extension fragment shader.
///
/// `buffer0` here must match `arguments.buffer0` in `TintSurface.metal`.
/// Other extensions may also use `buffer0` because argument IDs are local
/// to each extension draw.
public func registerArgumentBuffers(_ registry: RenderExtensionArgumentBufferRegistry) {
registry.registerArgumentBuffer(
RenderExtensionArgumentBufferDescriptor(
id: argumentLayoutID,
buffers: [
RenderExtensionArgumentBuffer(
id: RenderExtensionModelSurfaceArgument.buffer0
),
]
)
)
}

/// Creates the pipeline that combines the engine model vertex shader with
/// the extension's fragment shader.
///
/// Validation checks that the fragment shader uses the engine-owned outer
/// argument-buffer slot and that this layout was registered.
public func registerPipelines(_ registry: RenderPipelineRegistry) {
registry.registerModelSurfacePipeline(
pipelineID,
fragmentShader: "tintSurfaceFragment",
fragmentShaderLibrary: .registered(shaderLibraryID),
depthEnabled: true,
blendMode: .alphaPremultiplied,
name: "Tint Surface",
validation: .warn(argumentLayoutID: argumentLayoutID)
)
}

/// Adds the extension's required render work to the engine graph.
///
/// `buildGraph` is the only required protocol function besides `id`. The
/// engine calls it while constructing the graph; the pass closure executes
/// later during each frame.
public func buildGraph(
_ builder: inout RenderGraphBuilder,
context _: RenderGraphBuildContext
) {
builder.addPass(id: passID, stage: .beforePostProcess) { [pipelineID, argumentLayoutID] context in
// The helper supplies engine model bindings and draws only entities
// carrying TintSurfaceComponent. bindArguments runs once per entity.
context.drawModelSurfaceEntities(
pipeline: pipelineID,
matching: [TintSurfaceComponent.self],
label: "Tint Surface",
argumentLayoutID: argumentLayoutID,
bindArguments: { arguments, entityID, _ in
guard let tint = getEntityComponent(
entityId: entityID,
componentType: TintSurfaceComponent.self
) else {
return
}

var color = tint.color
// The Swift ID must match the Metal member read by the
// fragment shader. The engine owns the outer buffer slot.
arguments.setBytes(
&color,
id: RenderExtensionModelSurfaceArgument.buffer0
)
}
)
}
}
}
50 changes: 50 additions & 0 deletions Examples/RenderingExtensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Rendering Extension Examples

These examples demonstrate the two supported ways to integrate Rendering
Extensions with Untold Engine.

## Application-Local

[`ApplicationLocal`](ApplicationLocal/README.md) contains a focused model-surface
extension and Metal shader intended to be added directly to an application or
framework target.

Use it to learn:

- the `RenderExtension` authoring hooks;
- direct registration through `setRendering`;
- model-surface argument-buffer registration and binding;
- attaching an extension component to an entity.

## Swift Package Plugin

[`SwiftPackagePlugin`](SwiftPackagePlugin/README.md) is a complete, independently
buildable Swift package and an automated acceptance fixture.

Because `SwiftPackagePlugin` contains its own `Package.swift`, Xcode treats it as
a separate nested package and may not show its folder in the parent UntoldEngine
package navigator. The files are still available at
`Examples/RenderingExtensions/SwiftPackagePlugin`. Open that folder's
`Package.swift` separately, add it as a local package dependency, or include both
packages in an Xcode workspace. Do not add its source files directly to the
UntoldEngine target.

Use it to learn:

- package dependency and resource setup;
- precompiled metallib bundling;
- plugin manifests and public installation entry points;
- atomic installation of package-owned extensions;
- consumer-side component configuration.

> The package fixture uses water-themed names and placeholder shaders to exercise
> the complete API. It is not a real or production-ready water renderer.

Both examples use the same underlying `RenderExtension` API. Applications can
use application-local extensions and package plugins at the same time as long as
their globally registered artifact IDs are unique.

For the authoring guide, see
[Using Rendering Extensions](../../docs/API/UsingRenderingExtensions.md). For
internal behavior, see
[Rendering Extensions Architecture](../../docs/Architecture/RenderingExtensions.md).
Loading
Loading