Skip to content
Merged
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
19 changes: 19 additions & 0 deletions Sources/CShaderTypes/ShaderTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ typedef enum{
modelPassJointTransformIndex,
modelPassHasArmature,
modelPassOccluderShrinkIndex,
// The ModelInstanceUniform records of an instanced draw (vertexModelInstancedShader).
modelPassInstanceIndex,
}ModelPassBufferIndices;
typedef enum{
modelPassFragmentUniformIndex,
Expand All @@ -159,6 +161,21 @@ typedef enum{
modelPassFragmentSpotShadowUniformIndex,
}ModelPassFragmentBufferIndices;

// What one instance of an instanced model draw has of its own: its transforms. Draws that
// share their meshes, materials and ghost opacity are encoded once, with a record per
// instance; the draw's base instance is the index of its first record. Only the vertex
// stage reads the records; the fragment stage is fragmentModelShader, bound as for a
// single draw.
typedef struct
{
matrix_float4x4 modelViewMatrix;
matrix_float4x4 modelMatrix;
// The model matrix the instance was drawn with the previous frame, for the velocity
// (see Uniforms.prevModelMatrix).
matrix_float4x4 prevModelMatrix;
matrix_float3x3 normalMatrix;
} ModelInstanceUniform;

typedef enum{
deformationPassInPositionIndex,
deformationPassInNormalIndex,
Expand Down Expand Up @@ -655,6 +672,8 @@ typedef enum{
shadowPassLightPositionUniform,
shadowPassJointTransformIndex,
shadowPassHasArmature,
// The model matrix of each instance of an instanced shadow draw (vertexShadowInstancedShader).
shadowPassInstanceIndex,
}ShadowBufferIndices;

typedef struct{
Expand Down
28 changes: 27 additions & 1 deletion Sources/UntoldEngine/ECS/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,33 @@ public class WorldTransformComponent: Component {
}

public class RenderComponent: Component {
public var mesh: [Mesh]
public var mesh: [Mesh] {
didSet {
// Whatever changed (a material, a texture, the level of detail), the
// renderer has to look again at what the component draws.
instanceKey = .unresolved
if instancedSubmeshDraws != nil {
instancedSubmeshDraws = nil
}
if instancedShadowDraws != nil {
instancedShadowDraws = nil
}
}
}

/// What `mesh` draws, as a number the renderer groups instanced draws by: see
/// `RenderInstanceKeyTable`. Any change to `mesh` clears it.
var instanceKey: RenderInstanceKey = .unresolved

/// The draws of `mesh` in the opaque pass with everything they bind, prepared the
/// first time the component stands for its group in an instanced draw. Any change
/// to `mesh` clears them.
var instancedSubmeshDraws: [InstancedSubmeshDraw]?

/// The draws of `mesh` in a shadow pass, prepared the first time the component
/// stands for its group there: every submesh but those of glass, which casts no
/// shadow. Any change to `mesh` clears them.
var instancedShadowDraws: [InstancedShadowDraw]?
var assetURL: URL = .init(fileURLWithPath: "")
var assetName: String = ""
public var isVisible: Bool = true // Visibility flag for bulk loading
Expand Down
36 changes: 36 additions & 0 deletions Sources/UntoldEngine/Profiling/RenderStatsCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,42 @@ public extension MTLRenderCommandEncoder {
#endif
}

/// An instanced draw: `instanceCount` instances, the first of which reads the instance
/// record at `baseInstance`. The statistics count it as one draw call and
/// `instanceCount` times its triangles.
@inline(__always)
func drawIndexedPrimitivesTracked(
type: MTLPrimitiveType,
indexCount: Int,
indexType: MTLIndexType,
indexBuffer: MTLBuffer,
indexBufferOffset: Int,
instanceCount: Int,
baseInstance: Int,
category: RenderDrawCategory = .other
) {
drawIndexedPrimitives(
type: type,
indexCount: indexCount,
indexType: indexType,
indexBuffer: indexBuffer,
indexBufferOffset: indexBufferOffset,
instanceCount: instanceCount,
baseVertex: 0,
baseInstance: baseInstance
)
#if ENGINE_STATS_ENABLED
RenderStatsCollector.shared.recordIndexedDraw(
type: type,
indexCount: indexCount,
instanceCount: instanceCount,
category: category
)
#else
_ = category
#endif
}

@inline(__always)
func drawPrimitivesTracked(
type: MTLPrimitiveType,
Expand Down
56 changes: 50 additions & 6 deletions Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,24 @@ public func InitShadowPipeline() -> RenderPipeline? {
)
}

/// The shadow pipeline of instanced draws (see `InstancedDrawList`): the depth state of
/// `InitShadowPipeline`, a vertex stage that reads each instance's model matrix from a
/// buffer and takes the position stream alone. Nil on a device that cannot draw that
/// way (`InstancedDrawing.isSupported(by:)`): the pass then draws caster by caster.
public func InitShadowInstancedPipeline() -> RenderPipeline? {
guard let device = renderInfo.device, InstancedDrawing.isSupported(by: device) else { return nil }
return CreatePipeline(
vertexShader: "vertexShadowInstancedShader",
fragmentShader: nil,
vertexDescriptor: createShadowInstancedVertexDescriptor(),
colorFormats: [.invalid],
depthFormat: renderInfo.depthPixelFormat,
depthCompareFunction: MTLCompareFunction.less,
reverseZCompatible: false,
name: "Shadow Instanced Pipeline"
)
}

// MARK: Model pipeline

private var wf: WorkingColorFormats {
Expand All @@ -384,7 +402,30 @@ public func InitModelOpaquePipeline() -> RenderPipeline? {
makeModelPipeline(mayDiscard: false, name: "Model Pipeline (opaque)")
}

private func makeModelPipeline(mayDiscard: Bool, name: String) -> RenderPipeline? {
/// The model pipeline of instanced draws (see `InstancedDrawList`): the attachments, the
/// depth state, the function constants and the fragment function of `InitModelPipeline`
/// itself, so a group shades as a single draw does on every render path (deferred, hybrid,
/// forward); a vertex stage that reads each instance's transform from a
/// buffer, and the four vertex streams every mesh has. Nil on a device that cannot draw
/// that way (`InstancedDrawing.isSupported(by:)`): the pass then draws entity by entity.
public func InitModelInstancedPipeline() -> RenderPipeline? {
guard let device = renderInfo.device, InstancedDrawing.isSupported(by: device) else { return nil }
return makeModelPipeline(instanced: true, mayDiscard: true, name: "Model Instanced Pipeline")
}

/// The instanced model pipeline compiled without the discards, as `InitModelOpaquePipeline`:
/// the pass picks it for the groups whose material never needs them.
public func InitModelInstancedOpaquePipeline() -> RenderPipeline? {
guard let device = renderInfo.device, InstancedDrawing.isSupported(by: device) else { return nil }
return makeModelPipeline(instanced: true, mayDiscard: false, name: "Model Instanced Pipeline (opaque)")
}

private func makeModelPipeline(instanced: Bool = false, mayDiscard: Bool, name: String) -> RenderPipeline? {
let vertexShader = instanced ? "vertexModelInstancedShader" : "vertexModelShader"
// One fragment function for both: a second compilation of the same stage would differ
// from the first in the last bits it computes, and a group is to shade as a single
// draw does.
let fragmentShader = "fragmentModelShader"
let indirectInGBuffer = renderInfo.perFragmentIndirectLighting
let litInGBuffer = renderInfo.perFragmentLightingWanted
renderInfo.perFragmentIndirectLightingBuilt = indirectInGBuffer
Expand All @@ -393,8 +434,8 @@ private func makeModelPipeline(mayDiscard: Bool, name: String) -> RenderPipeline
handleError(.metalLibraryNotFound)
return nil
}
guard let vertexFunction = library.makeFunction(name: "vertexModelShader") else {
handleError(.shaderCreationFailed, "vertexModelShader")
guard let vertexFunction = library.makeFunction(name: vertexShader) else {
handleError(.shaderCreationFailed, vertexShader)
return nil
}
let constants = MTLFunctionConstantValues()
Expand All @@ -406,17 +447,17 @@ private func makeModelPipeline(mayDiscard: Bool, name: String) -> RenderPipeline
constants.setConstantValue(&litValue, type: .bool, index: Int(modelFunctionConstantLitInGBuffer.rawValue))
let fragmentFunction: MTLFunction
do {
fragmentFunction = try library.makeFunction(name: "fragmentModelShader", constantValues: constants)
fragmentFunction = try library.makeFunction(name: fragmentShader, constantValues: constants)
} catch {
handleError(.shaderCreationFailed, "fragmentModelShader (\(name)): \(error.localizedDescription)")
handleError(.shaderCreationFailed, "\(fragmentShader) (\(name)): \(error.localizedDescription)")
return nil
}

let desc = MTLRenderPipelineDescriptor()
desc.label = name
desc.vertexFunction = vertexFunction
desc.fragmentFunction = fragmentFunction
desc.vertexDescriptor = createModelVertexDescriptor()
desc.vertexDescriptor = instanced ? createModelInstancedVertexDescriptor() : createModelVertexDescriptor()
desc.depthAttachmentPixelFormat = renderInfo.depthPixelFormat
desc.rasterSampleCount = max(1, renderInfo.opaqueSampleCount)

Expand Down Expand Up @@ -1261,8 +1302,11 @@ public func DefaultPipeLines() -> [(RenderPipelineType, RenderPipelineInitBlock)
(.grid, InitGridPipeline),
(.sky, InitSkyPipeline),
(.shadow, InitShadowPipeline),
(.shadowInstanced, InitShadowInstancedPipeline),
(.model, InitModelPipeline),
(.modelOpaque, InitModelOpaquePipeline),
(.modelInstanced, InitModelInstancedPipeline),
(.modelInstancedOpaque, InitModelInstancedOpaquePipeline),
(.light, InitLightPipeline),
(.lightTile, InitLightTilePipeline),
(.geometry, InitGeometryPipeline),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ public extension RenderPipelineType {
static let grid: RenderPipelineType = "grid"
static let sky: RenderPipelineType = "sky"
static let shadow: RenderPipelineType = "shadow"
/// The shadow pipeline for instanced draws: casters that share their meshes, one draw.
static let shadowInstanced: RenderPipelineType = "shadowInstanced"
static let model: RenderPipelineType = "model"
/// The model pipeline compiled without fragment discards, for the opaque draws that never
/// need them: the GPU resolves visibility before shading them.
static let modelOpaque: RenderPipelineType = "modelOpaque"
/// The model pipeline for instanced draws: entities that share their meshes and
/// materials, one draw.
static let modelInstanced: RenderPipelineType = "modelInstanced"
/// The instanced model pipeline compiled without fragment discards, as `modelOpaque`.
static let modelInstancedOpaque: RenderPipelineType = "modelInstancedOpaque"
static let light: RenderPipelineType = "light"
/// The TBDR light pass as a tile stage: lights each pixel once per colour it holds.
static let lightTile: RenderPipelineType = "lightTile"
Expand Down
47 changes: 47 additions & 0 deletions Sources/UntoldEngine/Renderer/RenderInitializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ func updateOpaquePipelinesForSampleCount() {
if let modelOpaquePipeline = InitModelOpaquePipeline() {
PipelineManager.shared.update(rendererPipeLine: modelOpaquePipeline, forType: .modelOpaque)
}
// Only where the renderer was set up with them: the pass draws instanced when they are there.
if PipelineManager.shared.renderPipelinesByType[.modelInstanced] != nil,
let modelInstancedPipeline = InitModelInstancedPipeline()
{
PipelineManager.shared.update(rendererPipeLine: modelInstancedPipeline, forType: .modelInstanced)
}
if PipelineManager.shared.renderPipelinesByType[.modelInstancedOpaque] != nil,
let modelInstancedOpaquePipeline = InitModelInstancedOpaquePipeline()
{
PipelineManager.shared.update(rendererPipeLine: modelInstancedOpaquePipeline, forType: .modelInstancedOpaque)
}
if let lightPipeline = InitLightPipeline() {
PipelineManager.shared.update(rendererPipeLine: lightPipeline, forType: .light)
}
Expand Down Expand Up @@ -1417,6 +1428,42 @@ func initIBLResources() {
}
}

/// The vertex streams of an instanced shadow draw: the position stream alone, laid out as
/// `createShadowVertexDescriptor` lays it out.
func createShadowInstancedVertexDescriptor() -> MTLVertexDescriptor {
let vertexDescriptor = MTLVertexDescriptor()
let position = Int(shadowPassModelPositionIndex.rawValue)
vertexDescriptor.attributes[position].format = .float4
vertexDescriptor.attributes[position].bufferIndex = position
vertexDescriptor.attributes[position].offset = 0
vertexDescriptor.layouts[position].stride = MemoryLayout<simd_float4>.stride
vertexDescriptor.layouts[position].stepFunction = .perVertex
vertexDescriptor.layouts[position].stepRate = 1
return vertexDescriptor
}

/// The vertex streams of an instanced model draw: position, normal, texture coordinates
/// and tangent, laid out as `createModelVertexDescriptor` lays them out. A mesh drawn
/// instanced has no skeleton, so the joint streams are not part of the draw.
func createModelInstancedVertexDescriptor() -> MTLVertexDescriptor {
let vertexDescriptor = MTLVertexDescriptor()
let streams: [(index: Int, format: MTLVertexFormat, stride: Int)] = [
(Int(modelPassVerticesIndex.rawValue), .float4, MemoryLayout<simd_float4>.stride),
(Int(modelPassNormalIndex.rawValue), .float4, MemoryLayout<simd_float4>.stride),
(Int(modelPassUVIndex.rawValue), .float2, MemoryLayout<simd_float2>.stride),
(Int(modelPassTangentIndex.rawValue), .float4, MemoryLayout<simd_float4>.stride),
]
for stream in streams {
vertexDescriptor.attributes[stream.index].format = stream.format
vertexDescriptor.attributes[stream.index].bufferIndex = stream.index
vertexDescriptor.attributes[stream.index].offset = 0
vertexDescriptor.layouts[stream.index].stride = stream.stride
vertexDescriptor.layouts[stream.index].stepFunction = .perVertex
vertexDescriptor.layouts[stream.index].stepRate = 1
}
return vertexDescriptor
}

func createShadowVertexDescriptor() -> MTLVertexDescriptor {
// set the vertex descriptor
let vertexDescriptor = MTLVertexDescriptor()
Expand Down
Loading
Loading