DEV Community

Cover image for Ray Tracing on macOS: A Comprehensive Guide & Hands-On Tutorial
Siraj Razick
Siraj Razick

Posted on AI-assisted

Ray Tracing on macOS: A Comprehensive Guide & Hands-On Tutorial

Few notes that helped me while porting Quake engine to support Ray Tracing on Mac, sharing so others can benefit.

original game is here : https://buymeacoffee.com/plexydesk/e/593896

Table of Contents

  1. Overview
  2. Prerequisites
  3. Core Concepts
  4. Hands-On Tutorial: Step-by-Step
  5. Official Sample Analysis
  6. Troubleshooting
  7. References

===

1. Overview

What is Ray Tracing in Metal?

Ray tracing in Apple's Metal API is a hardware-accelerated rendering technique that simulates the physical behavior of light by tracing rays through a scene. Introduced with Metal 3 (WWDC 2022), Metal provides a native ray tracing subsystem built on top of acceleration structures, enabling real-time lighting, shadows, reflections, and global illumination effects.

Unlike traditional rasterization—which projects triangles onto a screen pixel by pixel—ray tracing casts rays from the camera (or light sources) into the scene, computing exact intersections with geometry. This produces physically accurate results including:

  • Soft shadows from area lights
  • Realistic reflections and refractions
  • Ambient occlusion and indirect lighting
  • Accurate depth-of-field effects

Supported Hardware

| Platform | Support Level | Details |
|==========|===============|=========|
| Apple Silicon (M-series) | Full hardware acceleration | All M1/M2/M3/M4 chips include dedicated ray tracing cores |
| Intel Macs (with Metal-capable GPUs) | Software fallback / limited | Some Intel-based Macs support ray tracing via software; performance varies |

Key Point: Hardware-accelerated ray tracing requires an Apple Silicon Mac (M1 or later). On Intel Macs, ray tracing may fall back to CPU/software implementations, which are significantly slower.

The Metal Ray Tracing Pipeline

+======================+    +=========================+    +====================+
| Geometry Data |---->|| Acceleration Structure |---->|| Ray Tracing Pass   |
| (Vertices,     |    || (BVH Tree)               |    || (Compute Shader)  |
|  Indices)      |    ||                         |    ||                    |
+======================+    +=========================+    +====================+
                                                             |
                                                     +-------v-------+
                                                     | Output Image  |
                                                     | (Texture)     |
                                                     +---------------+
Enter fullscreen mode Exit fullscreen mode

The workflow consists of three main stages:

  1. Build the Acceleration Structure (AS) — Organize geometry for efficient ray intersection tests
  2. Configure the Ray Tracing Command Encoder — Set up shaders, function tables, and resources
  3. Dispatch the Ray Generation Routine — Execute the compute shader that traces rays

===

2. Prerequisites

Minimum Requirements

| Requirement | Version |
|=============|=========|
| macOS | 13.0 (Ventura) or later |
| Xcode | 14.0 or later |
| Metal SDK | Metal 3+ |
| Hardware | Apple Silicon (M1/M2/M3/M4) recommended |

Checking Device Support

Before using ray tracing, always verify that the target device supports it:

// Swift — Check if the default device supports ray tracing
import Metal

let device = MTLCreateSystemDefaultDevice()!
if device.supportsFamily(.apple3) {
    print("Device supports Apple Silicon features including ray tracing")
} else {
    print("Ray tracing may not be hardware-accelerated on this device")
}
Enter fullscreen mode Exit fullscreen mode
// Objective-C
#import <Metal/Metal.h>

id<MTLDevice> device = MTLCreateSystemDefaultDevice();
if ([device supportsFamily:MTLGPUFamilyApple3]) {
    NSLog(@"Device supports ray tracing");
}
Enter fullscreen mode Exit fullscreen mode

The MTLGPUFamilyApple3 family identifier indicates Apple Silicon GPUs with full Metal 3 feature support, including hardware ray tracing.

===

3. Core Concepts

3.1 Acceleration Structures (AS)

An acceleration structure is a hierarchical data structure (typically a Bounding Volume Hierarchy, or BVH) that organizes scene geometry to enable fast ray-intersection queries. Without an AS, every ray would need to test against every triangle in the scene — O(n) complexity. With an AS, the search becomes approximately O(log n).

Key Classes

| Class | Purpose |
|=======|=========|
| MTLAccelerationStructureDescriptor | Describes the overall acceleration structure |
| MTLAccelerationStructureTriangleGeometryDescriptor | Describes triangle geometry within the AS |
| MTLAccelerationStructureBoundingBoxGeometryDescriptor | Describes bounding box geometry |
| MTLAccelerationStructureInstanceDescriptor | Describes instances (for instance-level AS) |
| MTLAccelerationStructureCommandEncoder | Encodes build, refit, and copy commands |

Building Triangle Geometry

// Swift — Create a triangle geometry descriptor
let triangleDescriptor = MTLAccelerationStructureTriangleGeometryDescriptor()
triangleDescriptor.triangleCount = numberOfTriangles
triangleDescriptor.vertexBuffer = vertexBuffer
triangleDescriptor.vertexBufferOffset = 0
triangleDescriptor.vertexStride = strideInBytes
triangleDescriptor.vertexFormat = .float3
triangleDescriptor.indexBuffer = indexBuffer
triangleDescriptor.indexBufferOffset = 0
triangleDescriptor.indexType = .uint16
Enter fullscreen mode Exit fullscreen mode
// Objective-C
MTLAccelerationStructureTriangleGeometryDescriptor *triangleDescriptor =
    [MTLAccelerationStructureTriangleGeometryDescriptor new];
triangleDescriptor.triangleCount = numberOfTriangles;
triangleDescriptor.vertexBuffer = vertexBuffer;
triangleDescriptor.vertexBufferOffset = 0;
triangleDescriptor.vertexStride = sizeof(Vertex);
triangleDescriptor.vertexFormat = MTLVertexFormatFloat3;
triangleDescriptor.indexBuffer = indexBuffer;
triangleDescriptor.indexBufferOffset = 0;
triangleDescriptor.indexType = MTLIndexTypeUInt16;
Enter fullscreen mode Exit fullscreen mode

Configuring the Overall AS Descriptor

// Swift — Configure the acceleration structure descriptor
let asDescriptor = MTLAccelerationStructureDescriptor()
asDescriptor.usage = .allowQuery // Options: .static, .dynamic, .allowQuery
asDescriptor.geometryDescriptors = [triangleDescriptor]

// Calculate required scratch buffer size
let scratchBufferSize = device.accelerationStructureScratchBufferWidth(
    descriptor: asDescriptor
)
let scratchBuffer = device.makeBuffer(length: Int(scratchBufferSize))!
Enter fullscreen mode Exit fullscreen mode

The usage property controls how the AS behaves at runtime:

  • .static (default): The AS will not change after building. Best performance.
  • .dynamic: The AS may be updated/refitted. Slightly slower queries.
  • .allowQuery: Enables ray casting queries against the AS. Required for ray tracing.

3.2 Ray Generation Routines

A ray generation routine is a compute shader entry point marked with the [[kernel]] attribute. It is responsible for:

  1. Computing the initial ray origin and direction for each pixel/thread
  2. Invoking ray tracing via the intersector
  3. Handling hit/miss cases and writing output color
// Metal Shading Language — Ray Generation Kernel
#include <metal_stdlib>
using namespace metal;

#define GEOMETRY_MASK_TRIANGLE 1
#define RAY_MASK_PRIMARY (GEOMETRY_MASK_TRIANGLE)

struct Uniforms {
    uint width;
    uint height;
    float4x4 viewMatrix;
    float4x4 projectionMatrix;
};

kernel void raygen(
    device unsigned char* outputImage [[buffer(0)]],
    constant Uniforms& uniforms [[buffer(1)]],
    device MTLAccelerationStructure* accelerationStructure [[buffer(2)]],
    uint3 gid [[thread_position_in_grid]]
) {
    uint x = gid.x;
    uint y = gid.y;

    if (x >= uniforms.width || y >= uniforms.height) return;

    // Compute ray origin and direction from screen coordinates
    float2 uv = float2(x, y) / float2(uniforms.width, uniforms.height) * 2.0 - 1.0;
    uv.y = -uv.y; // Flip Y

    // Simple pinhole camera ray setup
    float3 rayOrigin = float3(0.0, 1.0, 3.0);
    float3 rayDir = normalize(float3(uv, -1.0));

    // Define ray bounds
    float tmin = 0.01f;
    float tmax = 1000.0f;

    // Perform ray intersection query
    float3 hitPoint;
    float2 barycentrics;
    uint primitiveId;
    bool hit = accelerationStructure->intersect(
        rayOrigin, rayDir, tmin, tmax,
        RAY_MASK_PRIMARY, hitPoint, barycentrics, primitiveId
    );

    // Write result to output texture
    if (hit) {
        // Hit: shade the intersection point
        float3 color = float3(0.8, 0.2, 0.3); // Example hit color
        outputImage[(y * uniforms.width + x) * 4 + 0] = uint8_t(color.r * 255);
        outputImage[(y * uniforms.width + x) * 4 + 1] = uint8_t(color.g * 255);
        outputImage[(y * uniforms.width + x) * 4 + 2] = uint8_t(color.b * 255);
        outputImage[(y * uniforms.width + x) * 4 + 3] = 255;
    } else {
        // Miss: background color
        outputImage[(y * uniforms.width + x) * 4 + 0] = 0;
        outputImage[(y * uniforms.width + x) * 4 + 1] = 0;
        outputImage[(y * uniforms.width + x) * 4 + 2] = 0;
        outputImage[(y * uniforms.width + x) * 4 + 3] = 255;
    }
}
Enter fullscreen mode Exit fullscreen mode

3.3 Intersection & Miss Shaders

Metal supports custom intersection functions that allow you to define bespoke intersection logic beyond standard triangle hits. These are marked with the [[intersection]] attribute.

// Custom sphere intersection function
struct SphereIntersection {
    bool accept [[accept_intersection]];
    float distance [[distance]];
};

constant bool useResourcesBuffer = true;

[[intersection(bounding_box, triangle_data, instancing)]]
SphereIntersection sphereIntersectionFunction(
    float3 origin [[origin]],
    float3 direction [[direction]],
    float minDistance [[min_distance]],
    float maxDistance [[max_distance]],
    unsigned int primitiveId [[primitive_id]],
    unsigned int geometryIndex [[geometry_intersection_function_table_offset]],
    device void* resources [[buffer(0), function_constant(useResourcesBuffer)]]
) {
    // Mathematical sphere intersection test
    float3 oc = origin - spheres[primitiveId].origin;
    float a = dot(direction, direction);
    float b = 2.0f * dot(oc, direction);
    float c = dot(oc, oc) - spheres[primitiveId].radiusSquared;
    float disc = b * b - 4.0f * a * c;

    SphereIntersection ret;
    if (disc >= 0.0f) {
        float sqrtDisc = sqrt(disc);
        float t = (-b - sqrtDisc) / (2.0f * a);
        if (t >= minDistance && t <= maxDistance) {
            ret.accept = true;
            ret.distance = t;
        } else {
            ret.accept = false;
        }
    } else {
        ret.accept = false;
    }
    return ret;
}
Enter fullscreen mode Exit fullscreen mode

The intersection function keywords control what data Metal passes between pipeline stages:

| Keyword | Purpose |
|=========|=========|
| bounding_box | The intersector handles bounding box primitives |
| triangle_data | Barycentric coordinates are computed and passed |
| instancing | Instance acceleration structure traversal is used |

When you omit keywords, Metal stores less intermediate data, potentially improving performance.

===

4. Hands-On Tutorial: Step-by-Step

Step 1: Project Setup

  1. Open Xcode → File → New → Project
  2. Choose macOS → App (or iOS → App)
  3. Select Swift or Objective-C as the language
  4. In your project settings, ensure Metal Framework is linked:
    • Go to Target → General → Frameworks, Libraries, and Embedded Content
    • Add Metal.framework and MetalPerformanceShaders.framework
  5. Create a .metal file for your shaders:
    • File → New → File → Metal Shader File
    • Name it RayTracing.metal

Step 2: Device Detection

Before creating any ray tracing resources, check device capability:

import Metal

class RayTracingRenderer {
    let device: MTLDevice
    let commandQueue: MTLCommandQueue

    init() throws {
        guard let device = MTLCreateSystemDefaultDevice() else {
            throw NSError(domain: "RayTracing", code: 1,
                          userInfo: [NSLocalizedDescriptionKey:
                             "No Metal-capable device found"])
        }
        self.device = device

        // Verify Apple Silicon support
        guard device.supportsFamily(.apple3) else {
            throw NSError(domain: "RayTracing", code: 2,
                          userInfo: [NSLocalizedDescriptionKey:
                             "Device does not support hardware ray tracing"])
        }

        guard let commandQueue = device.makeCommandQueue() else {
            throw NSError(domain: "RayTracing", code: 3,
                          userInfo: [NSLocalizedDescriptionKey:
                             "Failed to create command queue"])
        }
        self.commandQueue = commandQueue
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Building the Acceleration Structure

This is the most critical step. You must build the AS before any ray tracing can occur.

// Swift — Complete AS build workflow

func buildAccelerationStructure() throws -> MTLAccelerationStructure {
    // 1. Create the triangle geometry descriptor
    let triangleDescriptor = MTLAccelerationStructureTriangleGeometryDescriptor()
    triangleDescriptor.triangleCount = triangleCount
    triangleDescriptor.vertexBuffer = vertexBuffer
    triangleDescriptor.vertexBufferOffset = 0
    triangleDescriptor.vertexStride = MemoryLayout<Vertex>.stride
    triangleDescriptor.vertexFormat = .float3
    triangleDescriptor.indexBuffer = indexBuffer
    triangleDescriptor.indexBufferOffset = 0
    triangleDescriptor.indexType = .uint16

    // 2. Configure the overall AS descriptor
    let asDescriptor = MTLAccelerationStructureDescriptor()
    asDescriptor.usage = .allowQuery
    asDescriptor.geometryDescriptors = [triangleDescriptor]

    // 3. Create the acceleration structure buffer
    let asSize = device.accelerationStructureSize(descriptor: asDescriptor)
    guard let accelerationStructure = device.makeAccelerationStructure(
        descriptor: asDescriptor
    ) else {
        throw NSError(domain: "RayTracing", code: 4,
                      userInfo: [NSLocalizedDescriptionKey:
                         "Failed to create acceleration structure"])
    }

    // 4. Allocate scratch buffer (temporary working memory)
    let scratchBufferSize = device.accelerationStructureScratchBufferWidth(
        descriptor: asDescriptor
    )
    let scratchBuffer = device.makeBuffer(length: Int(scratchBufferSize))!

    // 5. Encode the build command
    guard let commandBuffer = commandQueue.makeCommandBuffer() else {
        throw NSError(domain: "RayTracing", code: 5,
                      userInfo: [NSLocalizedDescriptionKey:
                         "Failed to make command buffer"])
    }

    guard let encoder = commandBuffer.makeAccelerationStructureCommandEncoder() else {
        throw NSError(domain: "RayTracing", code: 6,
                      userInfo: [NSLocalizedDescriptionKey:
                         "Failed to make acceleration structure command encoder"])
    }

    // The build method signature:
    // build(accelerationStructure:descriptor:scratchBuffer:scratchBufferOffset:)
    encoder.build(
        accelerationStructure: accelerationStructure,
        descriptor: asDescriptor,
        scratchBuffer: scratchBuffer,
        scratchBufferOffset: 0
    )

    encoder.endEncoding()
    commandBuffer.commit()
    commandBuffer.waitUntilCompleted()

    return accelerationStructure
}
Enter fullscreen mode Exit fullscreen mode

Why the scratch buffer? The acceleration structure build process requires temporary GPU memory to construct the BVH tree internally. The scratchBuffer provides this workspace. Its required size is queried via accelerationStructureScratchBufferWidth(descriptor:). After the build completes, the scratch buffer can be reused for subsequent builds or refits.

Step 4: Defining the Ray Gen Shader

Create your Metal shader file (RayTracing.metal):

// RayTracing.metal
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;

// Constants passed from the CPU
struct CameraParams {
    float4x4 viewMatrix;
    float4x4 projectionMatrix;
    float2 viewportSize;
};

struct RayPayload {
    float3 color;      // RGB color of the hit surface
    float3 normal;     // Surface normal at hit point
    float3 position;   // World-space position of the hit
    float distance;    // Distance from ray origin
    bool hit;          // Whether any geometry was hit
};

// Ray generation kernel — one thread per pixel
[[kernel]] void raygen(
    device RayPayload* payloads       [[buffer(0)]],
    device MTLAccelerationStructure* accelerationStructure [[buffer(1)]],
    constant CameraParams& params     [[buffer(2)]],
    uint2 gid                         [[thread_position_in_grid]]
) {
    uint x = gid.x;
    uint y = gid.y;

    if (x >= uint(params.viewportSize.x) || y >= uint(params.viewportSize.y))
        return;

    // --- Ray Setup ---
    // Map pixel coordinates to NDC [-1, 1]
    float2 ndc = (float2(x, y) / params.viewportSize) * 2.0 - 1.0;
    ndc.y = -ndc.y; // Flip Y for OpenGL-style coords

    // Build ray direction from camera matrices
    float4 clipPos = float4(ndc, 0.0, 1.0);
    float4 eyePos = inverse(params.projectionMatrix) * clipPos;
    eyePos = eyePos / eyePos.w;
    float4 worldPos = inverse(params.viewMatrix) * eyePos;
    float3 rayOrigin = params.viewMatrix[3].xyz; // Camera position
    float3 rayDirection = normalize(worldPos.xyz - rayOrigin);

    // --- Ray Query ---
    float tmin = 0.001f;
    float tmax = 1000.0f;
    uint rayFlags = 0xFF; // Test all geometry
    uint instanceMask = 0xFF;

    float3 hitPoint;
    float2 barycentrics;
    uint primitiveId;
    bool hit = accelerationStructure->intersect(
        rayOrigin, rayDirection, tmin, tmax,
        rayFlags, instanceMask,
        hitPoint, barycentrics, primitiveId
    );

    // --- Output ---
    RayPayload payload;
    payload.hit = hit;
    if (hit) {
        payload.position = hitPoint;
        payload.distance = length(hitPoint - rayOrigin);
        payload.color = float3(0.8f, 0.2f, 0.3f); // Placeholder shading
        payload.normal = float3(0.0f, 1.0f, 0.0f);
    } else {
        payload.color = float3(0.0f, 0.05f, 0.1f); // Sky color
        payload.normal = float3(0.0f);
        payload.position = float3(0.0f);
        payload.distance = tmax;
    }

    payloads[gid.y * uint(params.viewportSize.x) + gid.x] = payload;
}
Enter fullscreen mode Exit fullscreen mode

Compile the shader library:

// Swift — Compile and load the shader library
let defaultLibrary = device.makeDefaultLibrary()!
let raygenFunction = defaultLibrary.makeFunction(name: "raygen")!

// Create a compute pipeline state for the ray gen shader
let raygenPipeline = try device.makeComputePipelineState(
    function: raygenFunction
)
Enter fullscreen mode Exit fullscreen mode

Step 5: Dispatching the Ray Tracing Command

Now that you have an acceleration structure and a compiled ray gen shader, encode the actual ray tracing work:

// Swift — Encode and dispatch the ray tracing pass

func renderFrame(outputTexture: MTLTexture) throws {
    guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }

    // --- Ray Tracing Compute Pass ---
    guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return }

    // Set the pipeline state
    computeEncoder.setComputePipelineState(raygenPipeline)

    // Bind the acceleration structure (resource at buffer slot 1)
    computeEncoder.setAccelerationStructure(accelerationStructure, at: 1)

    // Bind the output texture
    computeEncoder.setTexture(outputTexture, at: 0)

    // Bind camera parameters
    var cameraParams = CameraParams(
        viewMatrix: viewMatrix,
        projectionMatrix: projectionMatrix,
        viewportSize: float2(Float(textureWidth), Float(textureHeight))
    )
    computeEncoder.setBytes(&cameraParams, length: MemoryLayout<CameraParams>.size, at: 2)

    // Calculate threadgroup dimensions (one thread per pixel)
    let threadsPerThreadgroup = MTLSize(width: 16, height: 16, depth: 1)
    let numThreadgroups = MTLSize(
        width: (textureWidth + 15) / 16,
        height: (textureHeight + 15) / 16,
        depth: 1
    )

    // Dispatch the ray gen kernel
    computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerThreadgroup)

    computeEncoder.endEncoding()

    // Present or blit the result
    // ... (presentation code depends on your view setup)

    commandBuffer.commit()
}
Enter fullscreen mode Exit fullscreen mode

Complete Workflow Summary:

1. Create MTLAccelerationStructureTriangleGeometryDescriptor
   |
2. Configure MTLAccelerationStructureDescriptor with usage=.allowQuery
   |
3. Create MTLAccelerationStructure buffer on the device
   |
4. Allocate scratch buffer (query size first)
   |
5. Encode build command via MTLAccelerationStructureCommandEncoder
   |
6. Commit command buffer and wait
   |
7. Create compute pipeline from ray gen shader
   |
8. Encode compute pass: set AS, textures, parameters
   |
9. Dispatch threadgroups (one per pixel or tile)
   |
10. Commit and present
Enter fullscreen mode Exit fullscreen mode

===

5. Official Sample Analysis

Analyzing the gfx-rs Metal Ray Tracing Sample

One of the most thorough open-source examples of Metal ray tracing is the gfx-rs metal-rs ray tracing sample, available at:

https://github.com/gfx-rs/metal-rs/tree/master/examples/raytracing

This sample demonstrates a complete path tracer implemented in Metal, including:

Key Implementation Details

1. Geometry Masks and Ray Types

#define GEOMETRY_MASK_TRIANGLE 1
#define GEOMETRY_MASK_SPHERE   2
#define GEOMETRY_MASK_LIGHT    4
#define GEOMETRY_MASK_GEOMETRY (GEOMETRY_MASK_TRIANGLE | GEOMETRY_MASK_SPHERE)
#define RAY_MASK_PRIMARY   (GEOMETRY_MASK_GEOMETRY | GEOMETRY_MASK_LIGHT)
#define RAY_MASK_SHADOW    GEOMETRY_MASK_GEOMETRY
#define RAY_MASK_SECONDARY GEOMETRY_MASK_GEOMETRY
Enter fullscreen mode Exit fullscreen mode

The sample uses bitmasks to filter which geometry types each ray should intersect. Primary rays test everything (geometry + lights), shadow rays only test geometry, and secondary rays (reflections/refractions) also only test geometry.

2. Custom Intersection Functions

The sample implements custom sphere intersection functions using the [[intersection]] attribute:

[[intersection(bounding_box, triangle_data, instancing)]]
BoundingBoxIntersection sphereIntersectionFunction(
    float3 origin [[origin]],
    float3 direction [[direction]],
    float minDistance [[min_distance]],
    float maxDistance [[max_distance]],
    unsigned int primitiveId [[primitive_id]],
    unsigned int geometryIndex [[geometry_intersection_function_table_offset]],
    device void* resources [[buffer(0), function_constant(useResourcesBuffer)]]
)
Enter fullscreen mode Exit fullscreen mode

This demonstrates how Metal allows you to define bespoke intersection logic for non-standard primitives (like spheres) while still leveraging the hardware BVH traversal for bounding box culling.

3. Per-Primitive Data

The sample shows two approaches to binding geometry data:

  • Per-primitive data: Each primitive has its own data buffer, accessed via the [[primitive_data]] attribute
  • Resources buffer: A single buffer indexed by geometry type
if (usePerPrimitiveData) {
    sphere = *(const device Sphere*)perPrimitiveData;
} else {
    device SphereResources& sphereResources =
        *(device SphereResources*)((device char*)resources + resourcesStride * geometryIndex);
    sphere = sphereResources.spheres[primitiveId];
}
Enter fullscreen mode Exit fullscreen mode

4. Monte Carlo Sampling

The sample implements cosine-weighted hemisphere sampling for diffuse shading:

inline float3 sampleCosineWeightedHemisphere(float2 u) {
    float phi = 2.0f * M_PI_F * u.x;
    float cos_phi;
    float sin_phi = sincos(phi, cos_phi);
    float cos_theta = sqrt(u.y);
    float sin_theta = sqrt(1.0f - cos_theta * cos_theta);
    return float3(sin_theta * cos_phi, cos_theta, sin_theta * sin_phi);
}
Enter fullscreen mode Exit fullscreen mode

5. Halton Sequence for Anti-Aliasing

float halton(unsigned int i, unsigned int d) {
    unsigned int b = primes[d];
    float f = 1.0f;
    float invB = 1.0f / b;
    float r = 0;
    while (i > 0) {
        f = f * invB;
        r = r + f * (i % b);
        i = i / b;
    }
    return r;
}
Enter fullscreen mode Exit fullscreen mode

This low-discrepancy sequence generates more evenly distributed samples than pure random numbers, reducing noise in rendered images.

Apple's Official Samples

Apple also provides official ray tracing samples through the Apple Developer portal:

  • "Accelerating ray tracing using Metal" — Demonstrates basic ray tracing with motion blur
  • "Rendering reflections in real time using ray tracing" — Implements real-time reflections via ray-traced compute passes
  • "Enhance visual quality with curves using Metal ray tracing" — Uses ray tracing for curve rendering

These samples are accessible at: https://developer.apple.com/metal/sample-code/

===

6. Troubleshooting

Common Errors and Solutions

| Error | Cause | Solution |
|=======|=======|==========|
| MTLDevice does not support ray tracing | Running on unsupported hardware or old macOS | Verify Apple Silicon + macOS 13+ |
| Invalid argument during build() | Mismatched buffer types or invalid descriptor | Ensure vertex/index buffers are storageModeShared or storageModePrivate |
| Function table not bound | Missing intersection function table setup | Call setIntersectionFunctionTable() on the compute encoder |
| Shader compilation error | Invalid Metal Shading Language syntax | Use Xcode's built-in Metal compiler diagnostics |
| Empty/black output image | Ray miss handling or incorrect dispatch | Verify ray bounds, check thread_position_in_grid, validate output texture format |
| Performance issues | Too many rays per frame, no multithreading | Tile-based dispatch, reduce samples per pixel, use temporal accumulation |

Debugging with Xcode GPU Frame Capture

Xcode provides powerful tools for debugging ray tracing pipelines:

  1. Enable Metal Validation:

    • Go to Product → Scheme → Edit Scheme
    • Select Run → Options → Metal Validation
    • Enable Validate Render/Compute Commands
  2. GPU Frame Capture:

    • Click the Debug → Start GPU Frame Capture menu
    • Run your application
    • Xcode will capture the entire GPU command stream
  3. Inspect the Command Buffer:

    • In the GPU debugger, expand the command buffer hierarchy
    • Navigate to the MTLAccelerationStructureCommandEncoder to inspect build parameters
    • View the acceleration structure's internal BVH representation
  4. Shader Debugger:

    • Set breakpoints in your Metal shader functions
    • Inspect thread groups, thread IDs, and resource bindings
    • Watch variables like hit, barycentrics, and primitiveId during ray intersection
  5. Counter Sampling:

   // Sample GPU counters for performance profiling
   let counterSet = MTLCounterSet.deviceCounterSet(device: device)!
   let timestampCounter = counterSet.counter(at: 0)!
   // Attach counters to your command buffer for timing analysis
Enter fullscreen mode Exit fullscreen mode

Verification Checklist

Before deploying, verify:

  • [ ] Device supports MTLGPUFamilyApple3
  • [ ] Acceleration structure build completed without errors
  • [ ] Scratch buffer size was correctly queried
  • [ ] Ray gen shader compiles successfully
  • [ ] Threadgroup dimensions cover all pixels
  • [ ] Output texture format matches display requirements
  • [ ] All buffers are properly retained and released
  • [ ] No race conditions between AS updates and ray queries

===

7. References

Official Apple Documentation

| Resource | URL |
|==========|=====|
| Ray Tracing with Acceleration Structures | https://developer.apple.com/documentation/metal/ray-tracing-with-acceleration-structures |
| Accelerating Ray Tracing Using Metal | https://developer.apple.com/documentation/metal/accelerating-ray-tracing-using-metal |
| MTLAccelerationStructureCommandEncoder | https://developer.apple.com/documentation/metal/mtlaccelerationstructurecommandencoder |
| MTLAccelerationStructureTriangleGeometryDescriptor | https://developer.apple.com/documentation/metal/mtlaccelerationstructuretrianglegeometrydescriptor |
| MTLAccelerationStructureUsage | https://developer.apple.com/documentation/metal/mtlaccelerationstructureusage |
| MTLDevice (supportsFamily) | https://developer.apple.com/documentation/metal/mtldevice |
| MPSAccelerationStructure | https://developer.apple.com/documentation/metalperformanceshaders/mpsaccelerationstructure |
| Control Ray Tracing with Intersection Queries | https://developer.apple.com/documentation/metal/control-the-ray-tracing-process-using-intersection-queries |

Official Sample Code

| Sample | URL |
|========|=====|
| Metal Sample Code (Apple Developer) | https://developer.apple.com/metal/sample-code/ |
| Metal FX Samples | https://developer.apple.com/metal/sample-code/ |
| Rendering Reflections in Real Time | https://developer.apple.com/documentation/metal/rendering-reflections-in-real-time-using-ray-tracing |
| Accelerating Ray Tracing and Motion Blur | https://developer.apple.com/documentation/metal/accelerating-ray-tracing-and-motion-blur-using-metal |

Community Resources

| Resource | URL |
|==========|=====|
| gfx-rs metal-rs Ray Tracing Sample | https://github.com/gfx-rs/metal-rs/tree/master/examples/raytracing |
| Apple Docs Mirror (livingston) | https://github.com/livingston/apple-docs |
| DeepWiki: Metal Ray Tracing | https://deepwiki.com/apple/metal-cpp/3.9-ray-tracing-and-acceleration-structures |
| Metal by Example | https://metalbyexample.com/ |

WWDC Sessions

| Session | Topic |
|=========|=======|
| WWDC22 | "Explore Metal 3" — Introduces hardware ray tracing |
| WWDC22 | "Advance graphics with MetalFX" — Upscaling and anti-aliasing |
| WWDC23 | "Improve rendering with Metal ray tracing" — Advanced techniques |

===

Top comments (0)