From b0ee1ee4c3af5b75da85a6078c21c7b31e6bf08c Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Tue, 10 Jun 2025 22:26:10 +1000 Subject: [PATCH] swift: Implement lessons 1-7 --- Package.swift | 41 +++ src/swift/Lesson1/lesson1.swift | 32 ++ src/swift/Lesson2/lesson2.swift | 172 ++++++++++ src/swift/Lesson3/lesson3.swift | 177 ++++++++++ src/swift/Lesson4/lesson4.swift | 183 +++++++++++ src/swift/Lesson5/lesson5.swift | 230 +++++++++++++ src/swift/Lesson6/lesson6.swift | 244 ++++++++++++++ src/swift/Lesson7/lesson7.swift | 323 +++++++++++++++++++ src/swift/NeHe/Application/AppConfig.swift | 28 ++ src/swift/NeHe/Application/AppDelegate.swift | 25 ++ src/swift/NeHe/Application/AppRunner.swift | 128 ++++++++ src/swift/NeHe/Matrix.swift | 96 ++++++ src/swift/NeHe/NeHeContext.swift | 229 +++++++++++++ src/swift/NeHe/NeHeCopyPass.swift | 270 ++++++++++++++++ src/swift/NeHe/NeHeError.swift | 24 ++ src/swift/NeHe/Size.swift | 30 ++ 16 files changed, 2232 insertions(+) create mode 100644 Package.swift create mode 100644 src/swift/Lesson1/lesson1.swift create mode 100644 src/swift/Lesson2/lesson2.swift create mode 100644 src/swift/Lesson3/lesson3.swift create mode 100644 src/swift/Lesson4/lesson4.swift create mode 100644 src/swift/Lesson5/lesson5.swift create mode 100644 src/swift/Lesson6/lesson6.swift create mode 100644 src/swift/Lesson7/lesson7.swift create mode 100644 src/swift/NeHe/Application/AppConfig.swift create mode 100644 src/swift/NeHe/Application/AppDelegate.swift create mode 100644 src/swift/NeHe/Application/AppRunner.swift create mode 100644 src/swift/NeHe/Matrix.swift create mode 100644 src/swift/NeHe/NeHeContext.swift create mode 100644 src/swift/NeHe/NeHeCopyPass.swift create mode 100644 src/swift/NeHe/NeHeError.swift create mode 100644 src/swift/NeHe/Size.swift diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..d280ce5 --- /dev/null +++ b/Package.swift @@ -0,0 +1,41 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "NeHe-SDL_GPU", + products: [ + .executable(name: "Lesson1", targets: [ "Lesson1" ]), + .executable(name: "Lesson2", targets: [ "Lesson2" ]), + .executable(name: "Lesson3", targets: [ "Lesson3" ]), + .executable(name: "Lesson4", targets: [ "Lesson4" ]), + .executable(name: "Lesson5", targets: [ "Lesson5" ]), + .executable(name: "Lesson6", targets: [ "Lesson6" ]), + .executable(name: "Lesson7", targets: [ "Lesson7" ]), + ], + dependencies: [ + .package(url: "https://github.com/GayPizzaSpecifications/SDL3Swift.git", branch: "main"), + ], + targets: [ + .target( + name: "NeHe", + dependencies: [ .product(name: "SDLSwift", package: "SDL3Swift") ], + path: "src/swift/NeHe"), + .executableTarget(name: "Lesson1", dependencies: [ "NeHe" ], path: "src/swift/Lesson1"), + .executableTarget(name: "Lesson2", dependencies: [ "NeHe" ], path: "src/swift/Lesson2", resources: [ + .process("../../../data/shaders/lesson2.metallib") ]), + .executableTarget(name: "Lesson3", dependencies: [ "NeHe" ], path: "src/swift/Lesson3", resources: [ + .process("../../../data/shaders/lesson3.metallib") ]), + .executableTarget(name: "Lesson4", dependencies: [ "NeHe" ], path: "src/swift/Lesson4", resources: [ + .process("../../../data/shaders/lesson3.metallib") ]), + .executableTarget(name: "Lesson5", dependencies: [ "NeHe" ], path: "src/swift/Lesson5", resources: [ + .process("../../../data/shaders/lesson3.metallib") ]), + .executableTarget(name: "Lesson6", dependencies: [ "NeHe" ], path: "src/swift/Lesson6", resources: [ + .process("../../../data/shaders/lesson6.metallib"), + .process("../../../data/NeHe.bmp") ]), + .executableTarget(name: "Lesson7", dependencies: [ "NeHe" ], path: "src/swift/Lesson7", resources: [ + .process("../../../data/shaders/lesson6.metallib"), + .process("../../../data/shaders/lesson7.metallib"), + .process("../../../data/Crate.bmp") ]) + ], +) diff --git a/src/swift/Lesson1/lesson1.swift b/src/swift/Lesson1/lesson1.swift new file mode 100644 index 0000000..4c118f6 --- /dev/null +++ b/src/swift/Lesson1/lesson1.swift @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import SDLSwift +import NeHe + +struct Lesson1: AppDelegate +{ + func draw(ctx: inout NeHeContext, cmd: OpaquePointer, + swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) + { + var colorInfo = SDL_GPUColorTargetInfo() + colorInfo.texture = swapchain + colorInfo.clear_color = SDL_FColor(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + colorInfo.load_op = SDL_GPU_LOADOP_CLEAR + colorInfo.store_op = SDL_GPU_STOREOP_STORE + + let pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, nil); + SDL_EndGPURenderPass(pass); + } +} + +@main struct Program: AppRunner +{ + typealias Delegate = Lesson1 + static let config = AppConfig( + title: "NeHe's OpenGL Framework", + width: 640, + height: 480) +} diff --git a/src/swift/Lesson2/lesson2.swift b/src/swift/Lesson2/lesson2.swift new file mode 100644 index 0000000..64cfefa --- /dev/null +++ b/src/swift/Lesson2/lesson2.swift @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import Foundation +import SDLSwift +import NeHe +import simd + +struct Lesson2: AppDelegate +{ + static let vertices = + [ + // Triangle + SIMD3( 0.0, 1.0, 0.0), // Top + SIMD3(-1.0, -1.0, 0.0), // Bottom left + SIMD3( 1.0, -1.0, 0.0), // Bottom right + // Quad + SIMD3( -1.0, 1.0, 0.0), // Top left + SIMD3( 1.0, 1.0, 0.0), // Top right + SIMD3( 1.0, -1.0, 0.0), // Bottom right + SIMD3( -1.0, -1.0, 0.0), // Bottom left + ] + + static let indices: [Int16] = + [ + // Triangle + 0, 1, 2, + // Quad + 3, 4, 5, 5, 6, 3, + ] + + var pso: OpaquePointer? = nil + var vtxBuffer: OpaquePointer? = nil + var idxBuffer: OpaquePointer? = nil + var projection: matrix_float4x4 = .init(1.0) + + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) + { + let (vertexShader, fragmentShader) = try ctx.loadShaders(name: "lesson2", vertexUniforms: 1) + defer + { + SDL_ReleaseGPUShader(ctx.device, fragmentShader) + SDL_ReleaseGPUShader(ctx.device, vertexShader) + } + + let vertexDescriptions: [SDL_GPUVertexBufferDescription] = + [ + SDL_GPUVertexBufferDescription( + slot: 0, + pitch: UInt32(MemoryLayout>.stride), + input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX, + instance_step_rate: 0), + ] + let vertexAttributes: [SDL_GPUVertexAttribute] = + [ + SDL_GPUVertexAttribute( + location: 0, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + offset: 0), + ] + let colourTargets: [SDL_GPUColorTargetDescription] = + [ + SDL_GPUColorTargetDescription( + format: SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window), + blend_state: SDL_GPUColorTargetBlendState()) + ] + var rasterizerDesc = SDL_GPURasterizerState() + rasterizerDesc.fill_mode = SDL_GPU_FILLMODE_FILL + rasterizerDesc.cull_mode = SDL_GPU_CULLMODE_NONE + rasterizerDesc.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + var targetInfo = SDL_GPUGraphicsPipelineTargetInfo() + targetInfo.color_target_descriptions = colourTargets.withUnsafeBufferPointer(\.baseAddress!) + targetInfo.num_color_targets = UInt32(colourTargets.count) + + var info = SDL_GPUGraphicsPipelineCreateInfo( + vertex_shader: vertexShader, + fragment_shader: fragmentShader, + vertex_input_state: SDL_GPUVertexInputState( + vertex_buffer_descriptions: vertexDescriptions.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_buffers: UInt32(vertexDescriptions.count), + vertex_attributes: vertexAttributes.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_attributes: UInt32(vertexAttributes.count)), + primitive_type: SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + rasterizer_state: rasterizerDesc, + multisample_state: SDL_GPUMultisampleState(), + depth_stencil_state: SDL_GPUDepthStencilState(), + target_info: targetInfo, + props: 0) + guard let pso = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else + { + throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) + } + self.pso = pso + + guard let cmd = SDL_AcquireGPUCommandBuffer(ctx.device) else + { + throw .sdlError("SDL_AcquireGPUCommandBuffer", String(cString: SDL_GetError())) + } + let pass = SDL_BeginGPUCopyPass(cmd) + defer + { + SDL_EndGPUCopyPass(pass) + SDL_SubmitGPUCommandBuffer(cmd) + } + + try ctx.copyPass { (pass) throws(NeHeError) in + self.vtxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_VERTEX, Self.vertices[...]) + self.idxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_INDEX, Self.indices[...]) + } + } + + func quit(ctx: NeHeContext) + { + SDL_ReleaseGPUBuffer(ctx.device, self.idxBuffer) + SDL_ReleaseGPUBuffer(ctx.device, self.vtxBuffer) + SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso) + } + + mutating func resize(size: Size) + { + let aspect = Float(size.width) / Float(max(1, size.height)) + self.projection = .perspective(fovy: 45, aspect: aspect, near: 0.1, far: 100) + } + + func draw(ctx: inout NeHeContext, cmd: OpaquePointer, + swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) + { + var colorInfo = SDL_GPUColorTargetInfo() + colorInfo.texture = swapchain + colorInfo.clear_color = SDL_FColor(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + colorInfo.load_op = SDL_GPU_LOADOP_CLEAR + colorInfo.store_op = SDL_GPU_STOREOP_STORE + + // Begin pass & bind pipeline state + let pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, nil); + SDL_BindGPUGraphicsPipeline(pass, self.pso) + + // Bind vertex & index buffers + let vtxBindings = [ SDL_GPUBufferBinding(buffer: self.vtxBuffer, offset: 0) ] + var idxBinding = SDL_GPUBufferBinding(buffer: self.idxBuffer, offset: 0) + SDL_BindGPUVertexBuffers(pass, 0, + vtxBindings.withUnsafeBufferPointer(\.baseAddress!), UInt32(vtxBindings.count)) + SDL_BindGPUIndexBuffer(pass, &idxBinding, SDL_GPU_INDEXELEMENTSIZE_16BIT) + + // Draw triangle 1.5 units to the left and 6 units into the camera + var model: simd_float4x4 = .translation(.init(-1.5, 0.0, -6.0)) + var viewProj = self.projection * model + SDL_PushGPUVertexUniformData(cmd, 0, &viewProj, UInt32(MemoryLayout.size)) + SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0) + + // Move to the right by 3 units and draw quad + model.translate(.init(3.0, 0.0, 0.0)) + viewProj = self.projection * model + SDL_PushGPUVertexUniformData(cmd, 0, &viewProj, UInt32(MemoryLayout.size)) + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0) + + SDL_EndGPURenderPass(pass); + } +} + +@main struct Program: AppRunner +{ + typealias Delegate = Lesson2 + static let config = AppConfig( + title: "NeHe's First Polygon Tutorial", + width: 640, + height: 480, + bundle: Bundle.module) +} diff --git a/src/swift/Lesson3/lesson3.swift b/src/swift/Lesson3/lesson3.swift new file mode 100644 index 0000000..cc73010 --- /dev/null +++ b/src/swift/Lesson3/lesson3.swift @@ -0,0 +1,177 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import Foundation +import SDLSwift +import NeHe +import simd + +struct Lesson3: AppDelegate +{ + struct Vertex + { + let position: SIMD3, color: SIMD4 + + init(_ position: SIMD3, _ color: SIMD4) + { + self.position = position + self.color = color + } + } + + static let vertices = + [ + // Triangle + Vertex(.init( 0.0, 1.0, 0.0), .init(1.0, 0.0, 0.0, 1.0)), // Top (red) + Vertex(.init(-1.0, -1.0, 0.0), .init(0.0, 1.0, 0.0, 1.0)), // Bottom left (green) + Vertex(.init( 1.0, -1.0, 0.0), .init(0.0, 0.0, 1.0, 1.0)), // Bottom right (blue) + // Quad + Vertex(.init(-1.0, 1.0, 0.0), .init(0.5, 0.5, 1.0, 1.0)), // Top left + Vertex(.init( 1.0, 1.0, 0.0), .init(0.5, 0.5, 1.0, 1.0)), // Top right + Vertex(.init( 1.0, -1.0, 0.0), .init(0.5, 0.5, 1.0, 1.0)), // Bottom right + Vertex(.init(-1.0, -1.0, 0.0), .init(0.5, 0.5, 1.0, 1.0)), // Bottom left + ] + + static let indices: [Int16] = + [ + // Triangle + 0, 1, 2, + // Quad + 3, 4, 5, 5, 6, 3, + ] + + var pso: OpaquePointer? = nil + var vtxBuffer: OpaquePointer? = nil + var idxBuffer: OpaquePointer? = nil + var projection: matrix_float4x4 = .init(1.0) + + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) + { + let (vertexShader, fragmentShader) = try ctx.loadShaders(name: "lesson3", vertexUniforms: 1) + defer + { + SDL_ReleaseGPUShader(ctx.device, fragmentShader) + SDL_ReleaseGPUShader(ctx.device, vertexShader) + } + + let vertexDescriptions: [SDL_GPUVertexBufferDescription] = + [ + SDL_GPUVertexBufferDescription( + slot: 0, + pitch: UInt32(MemoryLayout.stride), + input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX, + instance_step_rate: 0), + ] + let vertexAttributes: [SDL_GPUVertexAttribute] = + [ + SDL_GPUVertexAttribute( + location: 0, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + offset: UInt32(MemoryLayout.offset(of: \.position)!)), + SDL_GPUVertexAttribute( + location: 1, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, + offset: UInt32(MemoryLayout.offset(of: \.color)!)), + ] + let colourTargets: [SDL_GPUColorTargetDescription] = + [ + SDL_GPUColorTargetDescription( + format: SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window), + blend_state: SDL_GPUColorTargetBlendState()) + ] + var rasterizerDesc = SDL_GPURasterizerState() + rasterizerDesc.fill_mode = SDL_GPU_FILLMODE_FILL + rasterizerDesc.cull_mode = SDL_GPU_CULLMODE_NONE + rasterizerDesc.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + var targetInfo = SDL_GPUGraphicsPipelineTargetInfo() + targetInfo.color_target_descriptions = colourTargets.withUnsafeBufferPointer(\.baseAddress!) + targetInfo.num_color_targets = UInt32(colourTargets.count) + + var info = SDL_GPUGraphicsPipelineCreateInfo( + vertex_shader: vertexShader, + fragment_shader: fragmentShader, + vertex_input_state: SDL_GPUVertexInputState( + vertex_buffer_descriptions: vertexDescriptions.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_buffers: UInt32(vertexDescriptions.count), + vertex_attributes: vertexAttributes.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_attributes: UInt32(vertexAttributes.count)), + primitive_type: SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + rasterizer_state: rasterizerDesc, + multisample_state: SDL_GPUMultisampleState(), + depth_stencil_state: SDL_GPUDepthStencilState(), + target_info: targetInfo, + props: 0) + guard let pso = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else + { + throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) + } + self.pso = pso + + try ctx.copyPass { (pass) throws(NeHeError) in + self.vtxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_VERTEX, Self.vertices[...]) + self.idxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_INDEX, Self.indices[...]) + } + } + + func quit(ctx: NeHeContext) + { + SDL_ReleaseGPUBuffer(ctx.device, self.idxBuffer) + SDL_ReleaseGPUBuffer(ctx.device, self.vtxBuffer) + SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso) + } + + mutating func resize(size: Size) + { + let aspect = Float(size.width) / Float(max(1, size.height)) + self.projection = .perspective(fovy: 45, aspect: aspect, near: 0.1, far: 100) + } + + func draw(ctx: inout NeHeContext, cmd: OpaquePointer, + swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) + { + var colorInfo = SDL_GPUColorTargetInfo() + colorInfo.texture = swapchain + colorInfo.clear_color = SDL_FColor(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + colorInfo.load_op = SDL_GPU_LOADOP_CLEAR + colorInfo.store_op = SDL_GPU_STOREOP_STORE + + // Begin pass & bind pipeline state + let pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, nil); + SDL_BindGPUGraphicsPipeline(pass, self.pso) + + // Bind vertex & index buffers + let vtxBindings = [ SDL_GPUBufferBinding(buffer: self.vtxBuffer, offset: 0) ] + var idxBinding = SDL_GPUBufferBinding(buffer: self.idxBuffer, offset: 0) + SDL_BindGPUVertexBuffers(pass, 0, + vtxBindings.withUnsafeBufferPointer(\.baseAddress!), UInt32(vtxBindings.count)) + SDL_BindGPUIndexBuffer(pass, &idxBinding, SDL_GPU_INDEXELEMENTSIZE_16BIT) + + // Draw triangle 1.5 units to the left and 6 units into the camera + var model: simd_float4x4 = .translation(.init(-1.5, 0.0, -6.0)) + var viewProj = self.projection * model + SDL_PushGPUVertexUniformData(cmd, 0, &viewProj, UInt32(MemoryLayout.size)) + SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0) + + // Move to the right by 3 units and draw quad + model.translate(.init(3.0, 0.0, 0.0)) + viewProj = self.projection * model + SDL_PushGPUVertexUniformData(cmd, 0, &viewProj, UInt32(MemoryLayout.size)) + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0) + + SDL_EndGPURenderPass(pass); + } +} + +@main struct Program: AppRunner +{ + typealias Delegate = Lesson3 + static let config = AppConfig( + title: "NeHe's Color Tutorial", + width: 640, + height: 480, + bundle: Bundle.module) +} diff --git a/src/swift/Lesson4/lesson4.swift b/src/swift/Lesson4/lesson4.swift new file mode 100644 index 0000000..91ce03b --- /dev/null +++ b/src/swift/Lesson4/lesson4.swift @@ -0,0 +1,183 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import Foundation +import SDLSwift +import NeHe +import simd + +struct Lesson4: AppDelegate +{ + struct Vertex + { + let position: SIMD3, color: SIMD4 + + init(_ position: SIMD3, _ color: SIMD4) + { + self.position = position + self.color = color + } + } + static let vertices = + [ + // Triangle + Vertex(.init( 0.0, 1.0, 0.0), .init(1.0, 0.0, 0.0, 1.0)), // Top (red) + Vertex(.init(-1.0, -1.0, 0.0), .init(0.0, 1.0, 0.0, 1.0)), // Bottom left (green) + Vertex(.init( 1.0, -1.0, 0.0), .init(0.0, 0.0, 1.0, 1.0)), // Bottom right (blue) + // Quad + Vertex(.init(-1.0, 1.0, 0.0), .init(0.5, 0.5, 1.0, 1.0)), // Top left + Vertex(.init( 1.0, 1.0, 0.0), .init(0.5, 0.5, 1.0, 1.0)), // Top right + Vertex(.init( 1.0, -1.0, 0.0), .init(0.5, 0.5, 1.0, 1.0)), // Bottom right + Vertex(.init(-1.0, -1.0, 0.0), .init(0.5, 0.5, 1.0, 1.0)), // Bottom left + ] + + static let indices: [Int16] = + [ + // Triangle + 0, 1, 2, + // Quad + 3, 4, 5, 5, 6, 3, + ] + + var pso: OpaquePointer? = nil + var vtxBuffer: OpaquePointer? = nil + var idxBuffer: OpaquePointer? = nil + var projection: matrix_float4x4 = .init(1.0) + + var rotTri: Float = 0.0, rotQuad: Float = 0.0 + + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) + { + let (vertexShader, fragmentShader) = try ctx.loadShaders(name: "lesson3", vertexUniforms: 1) + defer + { + SDL_ReleaseGPUShader(ctx.device, fragmentShader) + SDL_ReleaseGPUShader(ctx.device, vertexShader) + } + + let vertexDescriptions: [SDL_GPUVertexBufferDescription] = + [ + SDL_GPUVertexBufferDescription( + slot: 0, + pitch: UInt32(MemoryLayout.stride), + input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX, + instance_step_rate: 0), + ] + let vertexAttributes: [SDL_GPUVertexAttribute] = + [ + SDL_GPUVertexAttribute( + location: 0, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + offset: UInt32(MemoryLayout.offset(of: \.position)!)), + SDL_GPUVertexAttribute( + location: 1, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, + offset: UInt32(MemoryLayout.offset(of: \.color)!)), + ] + let colourTargets: [SDL_GPUColorTargetDescription] = + [ + SDL_GPUColorTargetDescription( + format: SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window), + blend_state: SDL_GPUColorTargetBlendState()) + ] + var rasterizerDesc = SDL_GPURasterizerState() + rasterizerDesc.fill_mode = SDL_GPU_FILLMODE_FILL + rasterizerDesc.cull_mode = SDL_GPU_CULLMODE_NONE + rasterizerDesc.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + var targetInfo = SDL_GPUGraphicsPipelineTargetInfo() + targetInfo.color_target_descriptions = colourTargets.withUnsafeBufferPointer(\.baseAddress!) + targetInfo.num_color_targets = UInt32(colourTargets.count) + + var info = SDL_GPUGraphicsPipelineCreateInfo( + vertex_shader: vertexShader, + fragment_shader: fragmentShader, + vertex_input_state: SDL_GPUVertexInputState( + vertex_buffer_descriptions: vertexDescriptions.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_buffers: UInt32(vertexDescriptions.count), + vertex_attributes: vertexAttributes.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_attributes: UInt32(vertexAttributes.count)), + primitive_type: SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + rasterizer_state: rasterizerDesc, + multisample_state: SDL_GPUMultisampleState(), + depth_stencil_state: SDL_GPUDepthStencilState(), + target_info: targetInfo, + props: 0) + guard let pso = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else + { + throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) + } + self.pso = pso + + try ctx.copyPass { (pass) throws(NeHeError) in + self.vtxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_VERTEX, Self.vertices[...]) + self.idxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_INDEX, Self.indices[...]) + } + } + + func quit(ctx: NeHeContext) + { + SDL_ReleaseGPUBuffer(ctx.device, self.idxBuffer) + SDL_ReleaseGPUBuffer(ctx.device, self.vtxBuffer) + SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso) + } + + mutating func resize(size: Size) + { + let aspect = Float(size.width) / Float(max(1, size.height)) + self.projection = .perspective(fovy: 45, aspect: aspect, near: 0.1, far: 100) + } + + mutating func draw(ctx: inout NeHeContext, cmd: OpaquePointer, + swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) + { + var colorInfo = SDL_GPUColorTargetInfo() + colorInfo.texture = swapchain + colorInfo.clear_color = SDL_FColor(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + colorInfo.load_op = SDL_GPU_LOADOP_CLEAR + colorInfo.store_op = SDL_GPU_STOREOP_STORE + + // Begin pass & bind pipeline state + let pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, nil); + SDL_BindGPUGraphicsPipeline(pass, self.pso) + + // Bind vertex & index buffers + let vtxBindings = [ SDL_GPUBufferBinding(buffer: self.vtxBuffer, offset: 0) ] + var idxBinding = SDL_GPUBufferBinding(buffer: self.idxBuffer, offset: 0) + SDL_BindGPUVertexBuffers(pass, 0, + vtxBindings.withUnsafeBufferPointer(\.baseAddress!), UInt32(vtxBindings.count)) + SDL_BindGPUIndexBuffer(pass, &idxBinding, SDL_GPU_INDEXELEMENTSIZE_16BIT) + + // Draw triangle 1.5 units to the left and 6 units into the camera + var model: simd_float4x4 = .translation(.init(-1.5, 0.0, -6.0)) + model.rotate(angle: self.rotTri, axis: .init(0, 1, 0)) + var viewProj = self.projection * model + SDL_PushGPUVertexUniformData(cmd, 0, &viewProj, UInt32(MemoryLayout.size)) + SDL_DrawGPUIndexedPrimitives(pass, 3, 1, 0, 0, 0) + + // Draw quad 1.5 units to the right and 6 units in + model = .translation(.init(1.5, 0.0, -6.0)) + model.rotate(angle: self.rotQuad, axis: .init(1, 0, 0)) + viewProj = self.projection * model + SDL_PushGPUVertexUniformData(cmd, 0, &viewProj, UInt32(MemoryLayout.size)) + SDL_DrawGPUIndexedPrimitives(pass, 6, 1, 3, 0, 0) + + SDL_EndGPURenderPass(pass); + + self.rotTri += 0.2 + self.rotQuad -= 0.15 + } +} + +@main struct Program: AppRunner +{ + typealias Delegate = Lesson4 + static let config = AppConfig( + title: "NeHe's Rotation Tutorial", + width: 640, + height: 480, + bundle: Bundle.module) +} diff --git a/src/swift/Lesson5/lesson5.swift b/src/swift/Lesson5/lesson5.swift new file mode 100644 index 0000000..3b7f737 --- /dev/null +++ b/src/swift/Lesson5/lesson5.swift @@ -0,0 +1,230 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import Foundation +import SDLSwift +import NeHe +import simd + +struct Lesson5: AppDelegate +{ + struct Vertex + { + let position: SIMD3, color: SIMD4 + + init(_ position: SIMD3, _ color: SIMD4) + { + self.position = position + self.color = color + } + } + + static let vertices = + [ + // Pyramid + Vertex(.init( 0.0, 1.0, 0.0), .init(1.0, 0.0, 0.0, 1.0)), // Top of pyramid (Red) + Vertex(.init(-1.0, -1.0, 1.0), .init(0.0, 1.0, 0.0, 1.0)), // Front-left of pyramid (Green) + Vertex(.init( 1.0, -1.0, 1.0), .init(0.0, 0.0, 1.0, 1.0)), // Front-right of pyramid (Blue) + Vertex(.init( 1.0, -1.0, -1.0), .init(0.0, 1.0, 0.0, 1.0)), // Back-right of pyramid (Green) + Vertex(.init(-1.0, -1.0, -1.0), .init(0.0, 0.0, 1.0, 1.0)), // Back-left of pyramid (Blue) + // Cube + Vertex(.init( 1.0, 1.0, -1.0), .init(0.0, 1.0, 0.0, 1.0)), // Top-right of top face (Green) + Vertex(.init(-1.0, 1.0, -1.0), .init(0.0, 1.0, 0.0, 1.0)), // Top-left of top face (Green) + Vertex(.init(-1.0, 1.0, 1.0), .init(0.0, 1.0, 0.0, 1.0)), // Bottom-left of top face (Green) + Vertex(.init( 1.0, 1.0, 1.0), .init(0.0, 1.0, 0.0, 1.0)), // Bottom-right of top face (Green) + Vertex(.init( 1.0, -1.0, 1.0), .init(1.0, 0.5, 0.0, 1.0)), // Top-right of bottom face (Orange) + Vertex(.init(-1.0, -1.0, 1.0), .init(1.0, 0.5, 0.0, 1.0)), // Top-left of bottom face (Orange) + Vertex(.init(-1.0, -1.0, -1.0), .init(1.0, 0.5, 0.0, 1.0)), // Bottom-left of bottom face (Orange) + Vertex(.init( 1.0, -1.0, -1.0), .init(1.0, 0.5, 0.0, 1.0)), // Bottom-right of bottom face (Orange) + Vertex(.init( 1.0, 1.0, 1.0), .init(1.0, 0.0, 0.0, 1.0)), // Top-right of front face (Red) + Vertex(.init(-1.0, 1.0, 1.0), .init(1.0, 0.0, 0.0, 1.0)), // Top-left of front face (Red) + Vertex(.init(-1.0, -1.0, 1.0), .init(1.0, 0.0, 0.0, 1.0)), // Bottom-left of front face (Red) + Vertex(.init( 1.0, -1.0, 1.0), .init(1.0, 0.0, 0.0, 1.0)), // Bottom-right of front face (Red) + Vertex(.init( 1.0, -1.0, -1.0), .init(1.0, 1.0, 0.0, 1.0)), // Top-right of back face (Yellow) + Vertex(.init(-1.0, -1.0, -1.0), .init(1.0, 1.0, 0.0, 1.0)), // Top-left of back face (Yellow) + Vertex(.init(-1.0, 1.0, -1.0), .init(1.0, 1.0, 0.0, 1.0)), // Bottom-left of back face (Yellow) + Vertex(.init( 1.0, 1.0, -1.0), .init(1.0, 1.0, 0.0, 1.0)), // Bottom-right of back face (Yellow) + Vertex(.init(-1.0, 1.0, 1.0), .init(0.0, 0.0, 1.0, 1.0)), // Top-right of left face (Blue) + Vertex(.init(-1.0, 1.0, -1.0), .init(0.0, 0.0, 1.0, 1.0)), // Top-left of left face (Blue) + Vertex(.init(-1.0, -1.0, -1.0), .init(0.0, 0.0, 1.0, 1.0)), // Bottom-left of left face (Blue) + Vertex(.init(-1.0, -1.0, 1.0), .init(0.0, 0.0, 1.0, 1.0)), // Bottom-right of left face (Blue) + Vertex(.init( 1.0, 1.0, -1.0), .init(1.0, 0.0, 1.0, 1.0)), // Top-right of right face (Violet) + Vertex(.init( 1.0, 1.0, 1.0), .init(1.0, 0.0, 1.0, 1.0)), // Top-left of right face (Violet) + Vertex(.init( 1.0, -1.0, 1.0), .init(1.0, 0.0, 1.0, 1.0)), // Bottom-left of right face (Violet) + Vertex(.init( 1.0, -1.0, -1.0), .init(1.0, 0.0, 1.0, 1.0)), // Bottom-right of right face (Violet) + ] + + static let indices: [UInt16] = + [ + // Pyramid + 0, 1, 2, // Front + 0, 2, 3, // Right + 0, 3, 4, // Back + 0, 4, 1, // Left + // Cube + 5, 6, 7, 7, 8, 5, // Top + 9, 10, 11, 11, 12, 9, // Bottom + 13, 14, 15, 15, 16, 13, // Front + 17, 18, 19, 19, 20, 17, // Back + 21, 22, 23, 23, 24, 21, // Left + 25, 26, 27, 27, 28, 25, // Right + ] + + var pso: OpaquePointer? = nil + var vtxBuffer: OpaquePointer? = nil + var idxBuffer: OpaquePointer? = nil + var projection: matrix_float4x4 = .init(1.0) + + var rotTri: Float = 0.0, rotQuad: Float = 0.0 + + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) + { + let (vertexShader, fragmentShader) = try ctx.loadShaders(name: "lesson3", vertexUniforms: 1) + defer + { + SDL_ReleaseGPUShader(ctx.device, fragmentShader) + SDL_ReleaseGPUShader(ctx.device, vertexShader) + } + + let vertexDescriptions: [SDL_GPUVertexBufferDescription] = + [ + SDL_GPUVertexBufferDescription( + slot: 0, + pitch: UInt32(MemoryLayout.stride), + input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX, + instance_step_rate: 0), + ] + let vertexAttributes: [SDL_GPUVertexAttribute] = + [ + SDL_GPUVertexAttribute( + location: 0, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + offset: UInt32(MemoryLayout.offset(of: \.position)!)), + SDL_GPUVertexAttribute( + location: 1, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, + offset: UInt32(MemoryLayout.offset(of: \.color)!)), + ] + let colourTargets: [SDL_GPUColorTargetDescription] = + [ + SDL_GPUColorTargetDescription( + format: SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window), + blend_state: SDL_GPUColorTargetBlendState()) + ] + var rasterizerDesc = SDL_GPURasterizerState() + rasterizerDesc.fill_mode = SDL_GPU_FILLMODE_FILL + rasterizerDesc.cull_mode = SDL_GPU_CULLMODE_NONE + rasterizerDesc.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + var depthStencilState = SDL_GPUDepthStencilState() + depthStencilState.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL + depthStencilState.enable_depth_test = true + depthStencilState.enable_depth_write = true + var targetInfo = SDL_GPUGraphicsPipelineTargetInfo() + targetInfo.color_target_descriptions = colourTargets.withUnsafeBufferPointer(\.baseAddress!) + targetInfo.num_color_targets = UInt32(colourTargets.count) + targetInfo.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM + targetInfo.has_depth_stencil_target = true + + var info = SDL_GPUGraphicsPipelineCreateInfo( + vertex_shader: vertexShader, + fragment_shader: fragmentShader, + vertex_input_state: SDL_GPUVertexInputState( + vertex_buffer_descriptions: vertexDescriptions.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_buffers: UInt32(vertexDescriptions.count), + vertex_attributes: vertexAttributes.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_attributes: UInt32(vertexAttributes.count)), + primitive_type: SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + rasterizer_state: rasterizerDesc, + multisample_state: SDL_GPUMultisampleState(), + depth_stencil_state: depthStencilState, + target_info: targetInfo, + props: 0) + guard let pso = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else + { + throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) + } + self.pso = pso + + try ctx.copyPass { (pass) throws(NeHeError) in + self.vtxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_VERTEX, Self.vertices[...]) + self.idxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_INDEX, Self.indices[...]) + } + } + + func quit(ctx: NeHeContext) + { + SDL_ReleaseGPUBuffer(ctx.device, self.idxBuffer) + SDL_ReleaseGPUBuffer(ctx.device, self.vtxBuffer) + SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso) + } + + mutating func resize(size: Size) + { + let aspect = Float(size.width) / Float(max(1, size.height)) + self.projection = .perspective(fovy: 45, aspect: aspect, near: 0.1, far: 100) + } + + mutating func draw(ctx: inout NeHeContext, cmd: OpaquePointer, + swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) + { + var colorInfo = SDL_GPUColorTargetInfo() + colorInfo.texture = swapchain + colorInfo.clear_color = SDL_FColor(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + colorInfo.load_op = SDL_GPU_LOADOP_CLEAR + colorInfo.store_op = SDL_GPU_STOREOP_STORE + + var depthInfo = SDL_GPUDepthStencilTargetInfo() + depthInfo.texture = ctx.depthTexture + depthInfo.clear_depth = 1.0 + depthInfo.load_op = SDL_GPU_LOADOP_CLEAR + depthInfo.store_op = SDL_GPU_STOREOP_DONT_CARE + depthInfo.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE + depthInfo.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE + depthInfo.cycle = true + + // Begin pass & bind pipeline state + let pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo); + SDL_BindGPUGraphicsPipeline(pass, self.pso) + + // Bind vertex & index buffers + let vtxBindings = [ SDL_GPUBufferBinding(buffer: self.vtxBuffer, offset: 0) ] + var idxBinding = SDL_GPUBufferBinding(buffer: self.idxBuffer, offset: 0) + SDL_BindGPUVertexBuffers(pass, 0, + vtxBindings.withUnsafeBufferPointer(\.baseAddress!), UInt32(vtxBindings.count)) + SDL_BindGPUIndexBuffer(pass, &idxBinding, SDL_GPU_INDEXELEMENTSIZE_16BIT) + + // Draw triangle 1.5 units to the left and 6 units into the camera + var model: simd_float4x4 = .translation(.init(-1.5, 0.0, -6.0)) + model.rotate(angle: self.rotTri, axis: .init(0, 1, 0)) + var viewProj = self.projection * model + SDL_PushGPUVertexUniformData(cmd, 0, &viewProj, UInt32(MemoryLayout.size)) + SDL_DrawGPUIndexedPrimitives(pass, 12, 1, 0, 0, 0) + + // Draw quad 1.5 units to the right and 7 units into the camera + model = .translation(.init(1.5, 0.0, -7.0)) + model.rotate(angle: self.rotQuad, axis: .init(1, 1, 1)) + viewProj = self.projection * model + SDL_PushGPUVertexUniformData(cmd, 0, &viewProj, UInt32(MemoryLayout.size)) + SDL_DrawGPUIndexedPrimitives(pass, 36, 1, 12, 0, 0) + + SDL_EndGPURenderPass(pass); + + self.rotTri += 0.2 + self.rotQuad -= 0.15 + } +} + +@main struct Program: AppRunner +{ + typealias Delegate = Lesson5 + static let config = AppConfig( + title: "NeHe's Solid Object Tutorial", + width: 640, + height: 480, + createDepthBuffer: SDL_GPU_TEXTUREFORMAT_D16_UNORM, + bundle: Bundle.module) +} diff --git a/src/swift/Lesson6/lesson6.swift b/src/swift/Lesson6/lesson6.swift new file mode 100644 index 0000000..396c2c6 --- /dev/null +++ b/src/swift/Lesson6/lesson6.swift @@ -0,0 +1,244 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import Foundation +import SDLSwift +import NeHe +import simd + +struct Lesson6: AppDelegate +{ + struct Vertex + { + let position: SIMD3, texcoord: SIMD2 + + init(_ position: SIMD3, _ texcoord: SIMD2) + { + self.position = position + self.texcoord = texcoord + } + } + + static let vertices = + [ + // Front Face + Vertex(.init(-1.0, -1.0, 1.0), .init(0.0, 0.0)), + Vertex(.init( 1.0, -1.0, 1.0), .init(1.0, 0.0)), + Vertex(.init( 1.0, 1.0, 1.0), .init(1.0, 1.0)), + Vertex(.init(-1.0, 1.0, 1.0), .init(0.0, 1.0)), + // Back Face + Vertex(.init(-1.0, -1.0, -1.0), .init(1.0, 0.0)), + Vertex(.init(-1.0, 1.0, -1.0), .init(1.0, 1.0)), + Vertex(.init( 1.0, 1.0, -1.0), .init(0.0, 1.0)), + Vertex(.init( 1.0, -1.0, -1.0), .init(0.0, 0.0)), + // Top Face + Vertex(.init(-1.0, 1.0, -1.0), .init(0.0, 1.0)), + Vertex(.init(-1.0, 1.0, 1.0), .init(0.0, 0.0)), + Vertex(.init( 1.0, 1.0, 1.0), .init(1.0, 0.0)), + Vertex(.init( 1.0, 1.0, -1.0), .init(1.0, 1.0)), + // Bottom Face + Vertex(.init(-1.0, -1.0, -1.0), .init(1.0, 1.0)), + Vertex(.init( 1.0, -1.0, -1.0), .init(0.0, 1.0)), + Vertex(.init( 1.0, -1.0, 1.0), .init(0.0, 0.0)), + Vertex(.init(-1.0, -1.0, 1.0), .init(1.0, 0.0)), + // Right face + Vertex(.init( 1.0, -1.0, -1.0), .init(1.0, 0.0)), + Vertex(.init( 1.0, 1.0, -1.0), .init(1.0, 1.0)), + Vertex(.init( 1.0, 1.0, 1.0), .init(0.0, 1.0)), + Vertex(.init( 1.0, -1.0, 1.0), .init(0.0, 0.0)), + // Left Face + Vertex(.init(-1.0, -1.0, -1.0), .init(0.0, 0.0)), + Vertex(.init(-1.0, -1.0, 1.0), .init(1.0, 0.0)), + Vertex(.init(-1.0, 1.0, 1.0), .init(1.0, 1.0)), + Vertex(.init(-1.0, 1.0, -1.0), .init(0.0, 1.0)), + ] + + static let indices: [UInt16] = + [ + 0, 1, 2, 2, 3, 0, // Front + 4, 5, 6, 6, 7, 4, // Back + 8, 9, 10, 10, 11, 8, // Top + 12, 13, 14, 14, 15, 12, // Bottom + 16, 17, 18, 18, 19, 16, // Right + 20, 21, 22, 22, 23, 20 // Left + ] + + var pso: OpaquePointer? = nil + var vtxBuffer: OpaquePointer? = nil + var idxBuffer: OpaquePointer? = nil + var sampler: OpaquePointer? = nil + var texture: OpaquePointer? = nil + var projection: matrix_float4x4 = .init(1.0) + + var rot: SIMD3 = .init(repeating: 0.0) + + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) + { + let (vertexShader, fragmentShader) = try ctx.loadShaders(name: "lesson6", + vertexUniforms: 1, fragmentSamplers: 1) + defer + { + SDL_ReleaseGPUShader(ctx.device, fragmentShader) + SDL_ReleaseGPUShader(ctx.device, vertexShader) + } + + let vertexDescriptions: [SDL_GPUVertexBufferDescription] = + [ + SDL_GPUVertexBufferDescription( + slot: 0, + pitch: UInt32(MemoryLayout.stride), + input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX, + instance_step_rate: 0), + ] + let vertexAttributes: [SDL_GPUVertexAttribute] = + [ + SDL_GPUVertexAttribute( + location: 0, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + offset: UInt32(MemoryLayout.offset(of: \.position)!)), + SDL_GPUVertexAttribute( + location: 1, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, + offset: UInt32(MemoryLayout.offset(of: \.texcoord)!)), + ] + let colourTargets: [SDL_GPUColorTargetDescription] = + [ + SDL_GPUColorTargetDescription( + format: SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window), + blend_state: SDL_GPUColorTargetBlendState()) + ] + var rasterizerDesc = SDL_GPURasterizerState() + rasterizerDesc.fill_mode = SDL_GPU_FILLMODE_FILL + rasterizerDesc.cull_mode = SDL_GPU_CULLMODE_NONE + rasterizerDesc.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + var depthStencilState = SDL_GPUDepthStencilState() + depthStencilState.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL + depthStencilState.enable_depth_test = true + depthStencilState.enable_depth_write = true + var targetInfo = SDL_GPUGraphicsPipelineTargetInfo() + targetInfo.color_target_descriptions = colourTargets.withUnsafeBufferPointer(\.baseAddress!) + targetInfo.num_color_targets = UInt32(colourTargets.count) + targetInfo.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM + targetInfo.has_depth_stencil_target = true + + var info = SDL_GPUGraphicsPipelineCreateInfo( + vertex_shader: vertexShader, + fragment_shader: fragmentShader, + vertex_input_state: SDL_GPUVertexInputState( + vertex_buffer_descriptions: vertexDescriptions.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_buffers: UInt32(vertexDescriptions.count), + vertex_attributes: vertexAttributes.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_attributes: UInt32(vertexAttributes.count)), + primitive_type: SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + rasterizer_state: rasterizerDesc, + multisample_state: SDL_GPUMultisampleState(), + depth_stencil_state: depthStencilState, + target_info: targetInfo, + props: 0) + guard let pso = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else + { + throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) + } + self.pso = pso + + var samplerInfo = SDL_GPUSamplerCreateInfo() + samplerInfo.min_filter = SDL_GPU_FILTER_LINEAR + samplerInfo.mag_filter = SDL_GPU_FILTER_LINEAR + guard let sampler = SDL_CreateGPUSampler(ctx.device, &samplerInfo) else + { + throw .sdlError("SDL_CreateGPUSampler", String(cString: SDL_GetError())) + } + self.sampler = sampler + + try ctx.copyPass { (pass) throws(NeHeError) in + self.texture = try pass.createTextureFrom(bmpResource: "NeHe", flip: true) + self.vtxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_VERTEX, Self.vertices[...]) + self.idxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_INDEX, Self.indices[...]) + } + } + + func quit(ctx: NeHeContext) + { + SDL_ReleaseGPUBuffer(ctx.device, self.idxBuffer) + SDL_ReleaseGPUBuffer(ctx.device, self.vtxBuffer) + SDL_ReleaseGPUTexture(ctx.device, self.texture) + SDL_ReleaseGPUSampler(ctx.device, self.sampler) + SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.pso) + } + + mutating func resize(size: Size) + { + let aspect = Float(size.width) / Float(max(1, size.height)) + self.projection = .perspective(fovy: 45, aspect: aspect, near: 0.1, far: 100) + } + + mutating func draw(ctx: inout NeHeContext, + cmd: OpaquePointer, swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) + { + var colorInfo = SDL_GPUColorTargetInfo() + colorInfo.texture = swapchain + colorInfo.clear_color = SDL_FColor(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + colorInfo.load_op = SDL_GPU_LOADOP_CLEAR + colorInfo.store_op = SDL_GPU_STOREOP_STORE + + var depthInfo = SDL_GPUDepthStencilTargetInfo() + depthInfo.texture = ctx.depthTexture + depthInfo.clear_depth = 1.0 + depthInfo.load_op = SDL_GPU_LOADOP_CLEAR + depthInfo.store_op = SDL_GPU_STOREOP_DONT_CARE + depthInfo.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE + depthInfo.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE + depthInfo.cycle = true + + // Begin pass & bind pipeline state + let pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo); + SDL_BindGPUGraphicsPipeline(pass, self.pso) + + // Bind texture + var textureBinding = SDL_GPUTextureSamplerBinding(texture: self.texture, sampler: self.sampler) + SDL_BindGPUFragmentSamplers(pass, 0, &textureBinding, 1) + + // Bind vertex & index buffers + let vtxBindings = [ SDL_GPUBufferBinding(buffer: self.vtxBuffer, offset: 0) ] + var idxBinding = SDL_GPUBufferBinding(buffer: self.idxBuffer, offset: 0) + SDL_BindGPUVertexBuffers(pass, 0, + vtxBindings.withUnsafeBufferPointer(\.baseAddress!), UInt32(vtxBindings.count)) + SDL_BindGPUIndexBuffer(pass, &idxBinding, SDL_GPU_INDEXELEMENTSIZE_16BIT) + + struct Uniforms { var modelViewProj: simd_float4x4, color: SIMD4 } + + // Move cube 5 units into the screen and apply some rotations + var model: simd_float4x4 = .translation(.init(0.0, 0.0, -5.0)) + model.rotate(angle: rot.x, axis: .init(1.0, 0.0, 0.0)) + model.rotate(angle: rot.y, axis: .init(0.0, 1.0, 0.0)) + model.rotate(angle: rot.z, axis: .init(0.0, 0.0, 1.0)) + + // Push shader uniforms + var u = Uniforms( + modelViewProj: self.projection * model, + color: .init(repeating: 1.0)) + SDL_PushGPUVertexUniformData(cmd, 0, &u, UInt32(MemoryLayout.size)) + + // Draw textured cube + SDL_DrawGPUIndexedPrimitives(pass, UInt32(Self.indices.count), 1, 0, 0, 0) + + SDL_EndGPURenderPass(pass); + + self.rot += .init(0.3, 0.2, 0.4) + } +} + +@main struct Program: AppRunner +{ + typealias Delegate = Lesson6 + static let config = AppConfig( + title: "NeHe's Texture Mapping Tutorial", + width: 640, + height: 480, + createDepthBuffer: SDL_GPU_TEXTUREFORMAT_D16_UNORM, + bundle: Bundle.module) +} diff --git a/src/swift/Lesson7/lesson7.swift b/src/swift/Lesson7/lesson7.swift new file mode 100644 index 0000000..2c911c0 --- /dev/null +++ b/src/swift/Lesson7/lesson7.swift @@ -0,0 +1,323 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import Foundation +import SDLSwift +import NeHe +import simd + +struct Lesson7: AppDelegate +{ + struct Vertex + { + let position: SIMD3, normal: SIMD3, texcoord: SIMD2 + + init(_ position: SIMD3, _ normal: SIMD3, _ texcoord: SIMD2) + { + self.position = position + self.normal = normal + self.texcoord = texcoord + } + } + + static let vertices = + [ + // Front Face + Vertex(.init(-1.0, -1.0, 1.0), .init( 0.0, 0.0, 1.0), .init(0.0, 0.0)), + Vertex(.init( 1.0, -1.0, 1.0), .init( 0.0, 0.0, 1.0), .init(1.0, 0.0)), + Vertex(.init( 1.0, 1.0, 1.0), .init( 0.0, 0.0, 1.0), .init(1.0, 1.0)), + Vertex(.init(-1.0, 1.0, 1.0), .init( 0.0, 0.0, 1.0), .init(0.0, 1.0)), + // Back Face + Vertex(.init(-1.0, -1.0, -1.0), .init( 0.0, 0.0, -1.0), .init(1.0, 0.0)), + Vertex(.init(-1.0, 1.0, -1.0), .init( 0.0, 0.0, -1.0), .init(1.0, 1.0)), + Vertex(.init( 1.0, 1.0, -1.0), .init( 0.0, 0.0, -1.0), .init(0.0, 1.0)), + Vertex(.init( 1.0, -1.0, -1.0), .init( 0.0, 0.0, -1.0), .init(0.0, 0.0)), + // Top Face + Vertex(.init(-1.0, 1.0, -1.0), .init( 0.0, 1.0, 0.0), .init(0.0, 1.0)), + Vertex(.init(-1.0, 1.0, 1.0), .init( 0.0, 1.0, 0.0), .init(0.0, 0.0)), + Vertex(.init( 1.0, 1.0, 1.0), .init( 0.0, 1.0, 0.0), .init(1.0, 0.0)), + Vertex(.init( 1.0, 1.0, -1.0), .init( 0.0, 1.0, 0.0), .init(1.0, 1.0)), + // Bottom Face + Vertex(.init(-1.0, -1.0, -1.0), .init( 0.0, -1.0, 0.0), .init(1.0, 1.0)), + Vertex(.init( 1.0, -1.0, -1.0), .init( 0.0, -1.0, 0.0), .init(0.0, 1.0)), + Vertex(.init( 1.0, -1.0, 1.0), .init( 0.0, -1.0, 0.0), .init(0.0, 0.0)), + Vertex(.init(-1.0, -1.0, 1.0), .init( 0.0, -1.0, 0.0), .init(1.0, 0.0)), + // Right face + Vertex(.init( 1.0, -1.0, -1.0), .init( 1.0, 0.0, 0.0), .init(1.0, 0.0)), + Vertex(.init( 1.0, 1.0, -1.0), .init( 1.0, 0.0, 0.0), .init(1.0, 1.0)), + Vertex(.init( 1.0, 1.0, 1.0), .init( 1.0, 0.0, 0.0), .init(0.0, 1.0)), + Vertex(.init( 1.0, -1.0, 1.0), .init( 1.0, 0.0, 0.0), .init(0.0, 0.0)), + // Left Face + Vertex(.init(-1.0, -1.0, -1.0), .init(-1.0, 0.0, 0.0), .init(0.0, 0.0)), + Vertex(.init(-1.0, -1.0, 1.0), .init(-1.0, 0.0, 0.0), .init(1.0, 0.0)), + Vertex(.init(-1.0, 1.0, 1.0), .init(-1.0, 0.0, 0.0), .init(1.0, 1.0)), + Vertex(.init(-1.0, 1.0, -1.0), .init(-1.0, 0.0, 0.0), .init(0.0, 1.0)), + ] + + static let indices: [UInt16] = + [ + 0, 1, 2, 2, 3, 0, // Front + 4, 5, 6, 6, 7, 4, // Back + 8, 9, 10, 10, 11, 8, // Top + 12, 13, 14, 14, 15, 12, // Bottom + 16, 17, 18, 18, 19, 16, // Right + 20, 21, 22, 22, 23, 20 // Left + ] + + var psoUnlit: OpaquePointer? = nil + var psoLight: OpaquePointer? = nil + var vtxBuffer: OpaquePointer? = nil + var idxBuffer: OpaquePointer? = nil + var samplers = [OpaquePointer?](repeating: nil, count: 3) + var texture: OpaquePointer? = nil + var projection: matrix_float4x4 = .init(1.0) + + struct Light { let ambient: SIMD4, diffuse: SIMD4, position: SIMD4 } + + var lighting = false + var light = Light( + ambient: .init(0.5, 0.5, 0.5, 1.0), + diffuse: .init(1.0, 1.0, 1.0, 1.0), + position: .init(0.0, 0.0, 2.0, 1.0)) + var filter = 0 + + var rot = SIMD2(repeating: 0.0) + var speed = SIMD2(repeating: 0.0) + var z: Float = -5.0 + + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) + { + let (vertexShaderUnlit, fragmentShaderUnlit) = try ctx.loadShaders(name: "lesson6", + vertexUniforms: 1, fragmentSamplers: 1) + defer + { + SDL_ReleaseGPUShader(ctx.device, fragmentShaderUnlit) + SDL_ReleaseGPUShader(ctx.device, vertexShaderUnlit) + } + let (vertexShaderLight, fragmentShaderLight) = try ctx.loadShaders(name: "lesson7", + vertexUniforms: 2, fragmentSamplers: 1) + defer + { + SDL_ReleaseGPUShader(ctx.device, fragmentShaderLight) + SDL_ReleaseGPUShader(ctx.device, vertexShaderLight) + } + + let vertexDescriptions: [SDL_GPUVertexBufferDescription] = + [ + SDL_GPUVertexBufferDescription( + slot: 0, + pitch: UInt32(MemoryLayout.stride), + input_rate: SDL_GPU_VERTEXINPUTRATE_VERTEX, + instance_step_rate: 0), + ] + let vertexAttributes: [SDL_GPUVertexAttribute] = + [ + SDL_GPUVertexAttribute( + location: 0, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + offset: UInt32(MemoryLayout.offset(of: \.position)!)), + SDL_GPUVertexAttribute( + location: 2, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, + offset: UInt32(MemoryLayout.offset(of: \.normal)!)), + SDL_GPUVertexAttribute( + location: 1, + buffer_slot: 0, + format: SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, + offset: UInt32(MemoryLayout.offset(of: \.texcoord)!)), + ] + let colourTargets: [SDL_GPUColorTargetDescription] = + [ + SDL_GPUColorTargetDescription( + format: SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window), + blend_state: SDL_GPUColorTargetBlendState()) + ] + var rasterizerDesc = SDL_GPURasterizerState() + rasterizerDesc.fill_mode = SDL_GPU_FILLMODE_FILL + rasterizerDesc.cull_mode = SDL_GPU_CULLMODE_NONE + rasterizerDesc.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE + var depthStencilState = SDL_GPUDepthStencilState() + depthStencilState.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL + depthStencilState.enable_depth_test = true + depthStencilState.enable_depth_write = true + var targetInfo = SDL_GPUGraphicsPipelineTargetInfo() + targetInfo.color_target_descriptions = colourTargets.withUnsafeBufferPointer(\.baseAddress!) + targetInfo.num_color_targets = UInt32(colourTargets.count) + targetInfo.depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM + targetInfo.has_depth_stencil_target = true + + var info = SDL_GPUGraphicsPipelineCreateInfo() + info.vertex_input_state = SDL_GPUVertexInputState( + vertex_buffer_descriptions: vertexDescriptions.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_buffers: UInt32(vertexDescriptions.count), + vertex_attributes: vertexAttributes.withUnsafeBufferPointer(\.baseAddress!), + num_vertex_attributes: UInt32(vertexAttributes.count)) + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST + info.rasterizer_state = rasterizerDesc + info.depth_stencil_state = depthStencilState + info.target_info = targetInfo + + // Create unlit pipeline + info.vertex_shader = vertexShaderUnlit + info.fragment_shader = fragmentShaderUnlit + guard let psoUnlit = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else + { + throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) + } + self.psoUnlit = psoUnlit + + // Create lit pipeline + info.vertex_shader = vertexShaderLight + info.fragment_shader = fragmentShaderLight + guard let psoLight = SDL_CreateGPUGraphicsPipeline(ctx.device, &info) else + { + throw .sdlError("SDL_CreateGPUGraphicsPipeline", String(cString: SDL_GetError())) + } + self.psoLight = psoLight + + func createSampler( + filter: SDL_GPUFilter, + mipMode: SDL_GPUSamplerMipmapMode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST, + maxLod: Float = 0.0) throws(NeHeError) -> OpaquePointer + { + var samplerInfo = SDL_GPUSamplerCreateInfo() + samplerInfo.min_filter = filter + samplerInfo.mag_filter = filter + samplerInfo.mipmap_mode = mipMode + samplerInfo.max_lod = maxLod + guard let sampler = SDL_CreateGPUSampler(ctx.device, &samplerInfo) else + { + throw .sdlError("SDL_CreateGPUSampler", String(cString: SDL_GetError())) + } + return sampler + } + self.samplers[0] = try createSampler(filter: SDL_GPU_FILTER_NEAREST) + self.samplers[1] = try createSampler(filter: SDL_GPU_FILTER_LINEAR) + self.samplers[2] = try createSampler(filter: SDL_GPU_FILTER_LINEAR, maxLod: .greatestFiniteMagnitude) + + try ctx.copyPass { (pass) throws(NeHeError) in + self.texture = try pass.createTextureFrom(bmpResource: "Crate", flip: true, genMipmaps: true) + self.vtxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_VERTEX, Self.vertices[...]) + self.idxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_INDEX, Self.indices[...]) + } + } + + func quit(ctx: NeHeContext) + { + SDL_ReleaseGPUBuffer(ctx.device, self.idxBuffer) + SDL_ReleaseGPUBuffer(ctx.device, self.vtxBuffer) + SDL_ReleaseGPUTexture(ctx.device, self.texture) + self.samplers.reversed().forEach { SDL_ReleaseGPUSampler(ctx.device, $0) } + SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.psoLight) + SDL_ReleaseGPUGraphicsPipeline(ctx.device, self.psoUnlit) + } + + mutating func resize(size: Size) + { + let aspect = Float(size.width) / Float(max(1, size.height)) + self.projection = .perspective(fovy: 45, aspect: aspect, near: 0.1, far: 100) + } + + mutating func draw(ctx: inout NeHeContext, + cmd: OpaquePointer, swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) + { + var colorInfo = SDL_GPUColorTargetInfo() + colorInfo.texture = swapchain + colorInfo.clear_color = SDL_FColor(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + colorInfo.load_op = SDL_GPU_LOADOP_CLEAR + colorInfo.store_op = SDL_GPU_STOREOP_STORE + + var depthInfo = SDL_GPUDepthStencilTargetInfo() + depthInfo.texture = ctx.depthTexture + depthInfo.clear_depth = 1.0 + depthInfo.load_op = SDL_GPU_LOADOP_CLEAR + depthInfo.store_op = SDL_GPU_STOREOP_DONT_CARE + depthInfo.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE + depthInfo.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE + depthInfo.cycle = true + + // Begin pass & bind pipeline state + let pass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, &depthInfo); + SDL_BindGPUGraphicsPipeline(pass, self.lighting ? self.psoLight : self.psoUnlit) + + // Bind texture + var textureBinding = SDL_GPUTextureSamplerBinding(texture: self.texture, sampler: self.samplers[self.filter]) + SDL_BindGPUFragmentSamplers(pass, 0, &textureBinding, 1) + + // Bind vertex & index buffers + let vtxBindings = [ SDL_GPUBufferBinding(buffer: self.vtxBuffer, offset: 0) ] + var idxBinding = SDL_GPUBufferBinding(buffer: self.idxBuffer, offset: 0) + SDL_BindGPUVertexBuffers(pass, 0, + vtxBindings.withUnsafeBufferPointer(\.baseAddress!), UInt32(vtxBindings.count)) + SDL_BindGPUIndexBuffer(pass, &idxBinding, SDL_GPU_INDEXELEMENTSIZE_16BIT) + + // Setup the cube's model matrix + var model: simd_float4x4 = .translation(.init(0.0, 0.0, self.z)) + model.rotate(angle: rot.x, axis: .init(1.0, 0.0, 0.0)) + model.rotate(angle: rot.y, axis: .init(0.0, 1.0, 0.0)) + + // Push shader uniforms + if self.lighting + { + struct Uniforms { var model: simd_float4x4, projection: simd_float4x4 } + var u = Uniforms( + model: model, + projection: self.projection) + SDL_PushGPUVertexUniformData(cmd, 0, &u, UInt32(MemoryLayout.size)) + SDL_PushGPUVertexUniformData(cmd, 1, &self.light, UInt32(MemoryLayout.size)) + } + else + { + struct Uniforms { var modelViewProj: simd_float4x4, color: SIMD4 } + var u = Uniforms( + modelViewProj: self.projection * model, + color: .init(repeating: 1.0)) + SDL_PushGPUVertexUniformData(cmd, 0, &u, UInt32(MemoryLayout.size)) + } + + // Draw textured cube + SDL_DrawGPUIndexedPrimitives(pass, UInt32(Self.indices.count), 1, 0, 0, 0) + + SDL_EndGPURenderPass(pass); + + let keys = SDL_GetKeyboardState(nil)! + + if keys[Int(SDL_SCANCODE_PAGEUP.rawValue)] { self.z -= 0.02 } + if keys[Int(SDL_SCANCODE_PAGEDOWN.rawValue)] { self.z += 0.02 } + if keys[Int(SDL_SCANCODE_UP.rawValue)] { speed.x -= 0.01 } + if keys[Int(SDL_SCANCODE_DOWN.rawValue)] { speed.x += 0.01 } + if keys[Int(SDL_SCANCODE_RIGHT.rawValue)] { speed.y += 0.1 } + if keys[Int(SDL_SCANCODE_LEFT.rawValue)] { speed.y -= 0.1 } + + self.rot += self.speed + } + + mutating func key(ctx: inout NeHeContext, key: SDL_Keycode, down: Bool, repeat: Bool) + { + guard down && !`repeat` else { return } + switch key + { + case SDLK_L: + self.lighting = !self.lighting + case SDLK_F: + self.filter = (self.filter + 1) % self.samplers.count + default: + break + } + } +} + +@main struct Program: AppRunner +{ + typealias Delegate = Lesson7 + static let config = AppConfig( + title: "NeHe's Textures, Lighting & Keyboard Tutorial", + width: 640, + height: 480, + createDepthBuffer: SDL_GPU_TEXTUREFORMAT_D16_UNORM, + bundle: Bundle.module) +} diff --git a/src/swift/NeHe/Application/AppConfig.swift b/src/swift/NeHe/Application/AppConfig.swift new file mode 100644 index 0000000..216e4e9 --- /dev/null +++ b/src/swift/NeHe/Application/AppConfig.swift @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import SDLSwift +import Foundation + +public struct AppConfig: Sendable +{ + let title: StaticString + let width: Int32 + let height: Int32 + let manageDepthFormat: SDL_GPUTextureFormat + + let bundle: Bundle? + + public init(title: StaticString, width: Int32, height: Int32, + createDepthBuffer depthFormat: SDL_GPUTextureFormat? = nil, + bundle: Bundle? = nil) + { + self.title = title + self.width = width + self.height = height + self.manageDepthFormat = depthFormat ?? SDL_GPU_TEXTUREFORMAT_INVALID + self.bundle = bundle + } +} diff --git a/src/swift/NeHe/Application/AppDelegate.swift b/src/swift/NeHe/Application/AppDelegate.swift new file mode 100644 index 0000000..0d97627 --- /dev/null +++ b/src/swift/NeHe/Application/AppDelegate.swift @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import SDLSwift + +public protocol AppDelegate: ~Copyable +{ + init() + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) + mutating func quit(ctx: NeHeContext) + mutating func resize(size: Size) + mutating func draw(ctx: inout NeHeContext, cmd: OpaquePointer, + swapchain: OpaquePointer, swapchainSize: Size) throws(NeHeError) + mutating func key(ctx: inout NeHeContext, key: SDL_Keycode, down: Bool, repeat: Bool) +} + +public extension AppDelegate +{ + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) {} + mutating func quit(ctx: NeHeContext) {} + mutating func resize(size: Size) {} + mutating func key(ctx: inout NeHeContext, key: SDL_Keycode, down: Bool, repeat: Bool) {} +} diff --git a/src/swift/NeHe/Application/AppRunner.swift b/src/swift/NeHe/Application/AppRunner.swift new file mode 100644 index 0000000..f7d0f1d --- /dev/null +++ b/src/swift/NeHe/Application/AppRunner.swift @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import SDLSwift + +public protocol AppRunner +{ + associatedtype Delegate: AppDelegate + static var config: AppConfig { get } +} + +public extension AppRunner +{ + static func main() throws -> Void + { + // Initialise SDL + guard SDL_Init(SDL_INIT_VIDEO) else + { + throw NeHeError.sdlError("SDL_Init", String(cString: SDL_GetError())) + } + defer { SDL_Quit() } + + // Initialise GPU context + var ctx = try NeHeContext( + title: config.title, + width: config.width, + height: config.height, + bundle: config.bundle) + defer + { + SDL_ReleaseWindowFromGPUDevice(ctx.device, ctx.window) + SDL_DestroyGPUDevice(ctx.device) + SDL_DestroyWindow(ctx.window) + } + + // Handle depth buffer texture creation if requested + if config.manageDepthFormat != SDL_GPU_TEXTUREFORMAT_INVALID + { + var backbufWidth: Int32 = 0, backbufHeight: Int32 = 0 + SDL_GetWindowSizeInPixels(ctx.window, &backbufWidth, &backbufHeight) + try ctx.setupDepthTexture( + size: .init(UInt32(backbufWidth), UInt32(backbufHeight)), + format: config.manageDepthFormat) + } + defer + { + if config.manageDepthFormat != SDL_GPU_TEXTUREFORMAT_INVALID, + let depthTexture = ctx.depthTexture + { + SDL_ReleaseGPUTexture(ctx.device, depthTexture) + } + } + + // Initialise app delegate + var app = Delegate() + try app.`init`(ctx: &ctx) + + var fullscreen = false + + // Enter main loop + quit: while true + { + // Process events + var event = SDL_Event() + while SDL_PollEvent(&event) + { + switch SDL_EventType(event.type) + { + case SDL_EVENT_QUIT: + break quit + case SDL_EVENT_WINDOW_ENTER_FULLSCREEN, SDL_EVENT_WINDOW_LEAVE_FULLSCREEN: + fullscreen = event.type == SDL_EVENT_WINDOW_ENTER_FULLSCREEN.rawValue + case SDL_EVENT_KEY_DOWN: + if event.key.key == SDLK_ESCAPE + { + break quit + } + if event.key.key == SDLK_F1 + { + SDL_SetWindowFullscreen(ctx.window, !fullscreen) + break + } + fallthrough + case SDL_EVENT_KEY_UP: + app.key(ctx: &ctx, key: event.key.key, down: event.key.down, repeat: event.key.repeat) + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + app.resize(size: .init(width: event.window.data1, height: event.window.data2)) + default: + break + } + } + + guard let cmd = SDL_AcquireGPUCommandBuffer(ctx.device) else + { + throw NeHeError.sdlError("SDL_AcquireGPUCommandBuffer", String(cString: SDL_GetError())) + } + + var swapchainTexture: OpaquePointer?, swapchainWidth: Int32 = 0, swapchainHeight: Int32 = 0 + guard SDL_WaitAndAcquireGPUSwapchainTexture(cmd, ctx.window, + &swapchainTexture, &swapchainWidth, &swapchainHeight) else + { + let message = String(cString: SDL_GetError()) + SDL_CancelGPUCommandBuffer(cmd) + throw NeHeError.sdlError("SDL_WaitAndAcquireGPUSwapchainTexture", message) + } + + guard let swapchain = swapchainTexture else + { + SDL_CancelGPUCommandBuffer(cmd) + continue + } + + let swapchainSize = Size(width: UInt32(swapchainWidth), height: UInt32(swapchainHeight)) + + if config.manageDepthFormat != SDL_GPU_TEXTUREFORMAT_INVALID + && ctx.depthTexture != nil + && ctx.depthTextureSize != swapchainSize + { + try ctx.setupDepthTexture(size: swapchainSize) + } + + try app.draw(ctx: &ctx, cmd: cmd, swapchain: swapchain, swapchainSize: swapchainSize) + SDL_SubmitGPUCommandBuffer(cmd) + } + } +} diff --git a/src/swift/NeHe/Matrix.swift b/src/swift/NeHe/Matrix.swift new file mode 100644 index 0000000..5208ac7 --- /dev/null +++ b/src/swift/NeHe/Matrix.swift @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import simd + +public extension simd_float4x4 +{ + static let identity: Self = matrix_identity_float4x4 + + @inlinable static func translation(_ position: SIMD3) -> Self + { + Self( + .init( 1, 0, 0, 0), + .init( 0, 1, 0, 0), + .init( 0, 0, 1, 0), + .init(position, 1)) + } + + static func rotation(angle: Float, axis: SIMD3) -> Self + { + let r = simd_float3x3.makeGLRotation(angle, axis) + return Self( + .init(r.columns.0, 0), + .init(r.columns.1, 0), + .init(r.columns.2, 0), + .init( 0, 0, 0, 1)) + } + + static func perspective(fovy: Float, aspect: Float, near: Float, far: Float) -> Self + { + let h = 1 / tan(fovy * (.pi / 180) * 0.5) + let w = h / aspect + let invClipRng = 1 / (far - near) + let zh = -(far + near) * invClipRng + let zl = -(2 * far * near) * invClipRng + + return Self( + .init(w, 0, 0, 0), + .init(0, h, 0, 0), + .init(0, 0, zh, -1), + .init(0, 0, zl, 0)) + } + + @inlinable mutating func translate(_ offset: SIMD3) + { + /* + m = { [1 0 0 x] + [0 1 0 y] + [0 0 1 z] + [0 0 0 1] } * m + */ + self.columns.3 += + offset.x * self.columns.0 + + offset.y * self.columns.1 + + offset.z * self.columns.2 + } + + mutating func rotate(angle: Float, axis: SIMD3) + { + let r = simd_float3x3.makeGLRotation(angle, axis) + + // Set up temporaries + let (t0, t1, t2) = (self.columns.0, self.columns.1, self.columns.2) + + // Partial matrix multiplication + self.columns.0 = r.columns.0.x * t0 + r.columns.0.y * t1 + r.columns.0.z * t2 + self.columns.1 = r.columns.1.x * t0 + r.columns.1.y * t1 + r.columns.1.z * t2 + self.columns.2 = r.columns.2.x * t0 + r.columns.2.y * t1 + r.columns.2.z * t2 + } +} + +fileprivate extension simd_float3x3 +{ + static func makeRotation(_ c: Float, _ s: Float, _ axis: SIMD3) -> Self + { + let rc = 1 - c + let rcv = rc * axis, sv = s * axis + return Self( + rcv * axis.x + .init( +c, +sv.z, -sv.y), + rcv * axis.y + .init(-sv.z, +c, +sv.x), + rcv * axis.z + .init(+sv.y, -sv.x, +c)) + } + + static func makeGLRotation(_ angle: Float, _ axis: SIMD3) -> Self + { + // Treat inputs like glRotatef + let theta = angle * (.pi / 180) + let axisMag = simd_length(axis) + let n = (abs(axisMag - 1) > .ulpOfOne) + ? axis / axisMag + : axis + return Self.makeRotation(cos(theta), sin(theta), n) + } +} diff --git a/src/swift/NeHe/NeHeContext.swift b/src/swift/NeHe/NeHeContext.swift new file mode 100644 index 0000000..dfc4cda --- /dev/null +++ b/src/swift/NeHe/NeHeContext.swift @@ -0,0 +1,229 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import SDLSwift +import Foundation + +public struct NeHeContext +{ + public var window: OpaquePointer + public var device: OpaquePointer + public var depthTexture: OpaquePointer? + public var depthTextureSize: Size + public let bundle: Bundle? +} + +internal extension NeHeContext +{ + init(title: StaticString, width: Int32, height: Int32, bundle: Bundle?) throws(NeHeError) + { + // Create window + let flags = SDL_WindowFlags(SDL_WINDOW_RESIZABLE) | SDL_WindowFlags(SDL_WINDOW_HIGH_PIXEL_DENSITY) + guard let window = SDL_CreateWindow(title.utf8Start, width, height, flags) else + { + throw .sdlError("SDL_CreateWindow", String(cString: SDL_GetError())) + } + + // Open GPU device + let formats = + SDL_GPU_SHADERFORMAT_METALLIB | SDL_GPU_SHADERFORMAT_MSL | + SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL + guard let device = SDL_CreateGPUDevice(formats, true, nil) else + { + throw .sdlError("SDL_CreateGPUDevice", String(cString: SDL_GetError())) + } + + // Attach window to the GPU device + guard SDL_ClaimWindowForGPUDevice(device, window) else + { + throw .sdlError("SDL_ClaimWindowForGPUDevice", String(cString: SDL_GetError())) + } + + // Enable VSync + SDL_SetGPUSwapchainParameters(device, window, + SDL_GPU_SWAPCHAINCOMPOSITION_SDR, + SDL_GPU_PRESENTMODE_VSYNC) + + self.window = window + self.device = device + self.depthTexture = nil + self.depthTextureSize = .zero + self.bundle = bundle + } +} + +public extension NeHeContext +{ + mutating func setupDepthTexture(size: Size, + format: SDL_GPUTextureFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM, clearDepth: Float = 1.0) throws(NeHeError) + { + if self.depthTexture != nil + { + SDL_ReleaseGPUTexture(self.device, self.depthTexture) + self.depthTexture = nil + } + + let props = SDL_CreateProperties() + guard props != 0 else + { + throw .sdlError("SDL_CreateProperties", String(cString: SDL_GetError())) + } + // Workaround for https://github.com/libsdl-org/SDL/issues/10758 + SDL_SetFloatProperty(props, SDL_PROP_GPU_TEXTURE_CREATE_D3D12_CLEAR_DEPTH_FLOAT, clearDepth) + + var info = SDL_GPUTextureCreateInfo( + type: SDL_GPU_TEXTURETYPE_2D, + format: format, + usage: SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET, + width: size.width, + height: size.height, + layer_count_or_depth: 1, + num_levels: 1, + sample_count: SDL_GPU_SAMPLECOUNT_1, + props: props) + let newTexture = SDL_CreateGPUTexture(self.device, &info) + SDL_DestroyProperties(props) + guard let texture = newTexture else + { + throw .sdlError("SDL_CreateGPUTexture", String(cString: SDL_GetError())) + } + self.depthTexture = texture + self.depthTextureSize = size + } + + func loadShaders(name: String, vertexUniforms: UInt32 = 0, vertexStorage: UInt32 = 0, fragmentSamplers: UInt32 = 0) + throws(NeHeError) -> (vertex: OpaquePointer, fragment: OpaquePointer) + { + guard let bundle = self.bundle else { throw .fatalError("No bundle") } + + var info = ShaderProgramCreateInfo(format: SDL_GPUShaderFormat(SDL_GPU_SHADERFORMAT_INVALID), + vertexUniforms: vertexUniforms, + vertexStorage: vertexStorage, + fragmentSamplers: fragmentSamplers) + + let availableFormats = SDL_GetGPUShaderFormats(self.device) + if availableFormats & (SDL_GPU_SHADERFORMAT_METALLIB | SDL_GPU_SHADERFORMAT_MSL) != 0 + { + if availableFormats & SDL_GPU_SHADERFORMAT_METALLIB == SDL_GPU_SHADERFORMAT_METALLIB + { + if let path = bundle.url(forResource: name, withExtension: "metallib"), + let lib = try? Data(contentsOf: path) + { + info.format = SDL_GPU_SHADERFORMAT_METALLIB + return ( + try loadShaderBlob(lib, info, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain"), + try loadShaderBlob(lib, info, SDL_GPU_SHADERSTAGE_FRAGMENT, "FragmentMain")) + } + } + if availableFormats & SDL_GPU_SHADERFORMAT_MSL == SDL_GPU_SHADERFORMAT_MSL + { + guard let path = bundle.url(forResource: name, withExtension: "metal"), + let src = try? Data(contentsOf: path) else + { + throw .fatalError("Failed to load metal shader") + } + info.format = SDL_GPU_SHADERFORMAT_MSL + return ( + try loadShaderBlob(src, info, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain"), + try loadShaderBlob(src, info, SDL_GPU_SHADERSTAGE_FRAGMENT, "FragmentMain")) + } + } + else if availableFormats & SDL_GPU_SHADERFORMAT_SPIRV == SDL_GPU_SHADERFORMAT_SPIRV + { + info.format = SDL_GPU_SHADERFORMAT_SPIRV + return ( + try loadShader(bundle.url(forResource: name, withExtension: "vtx.spv"), + info, SDL_GPU_SHADERSTAGE_VERTEX, "main"), + try loadShader(bundle.url(forResource: name, withExtension: "frg.spv"), + info, SDL_GPU_SHADERSTAGE_FRAGMENT, "main")) + } + else if availableFormats & SDL_GPU_SHADERFORMAT_DXIL == SDL_GPU_SHADERFORMAT_DXIL + { + info.format = SDL_GPU_SHADERFORMAT_DXIL + return ( + try loadShader(bundle.url(forResource: name, withExtension: "vtx.dxb"), + info, SDL_GPU_SHADERSTAGE_VERTEX, "VertexMain"), + try loadShader(bundle.url(forResource: name, withExtension: "pxl.dxb"), + info, SDL_GPU_SHADERSTAGE_FRAGMENT, "PixelMain")) + + } + throw .fatalError("No supported shader formats found") + } + + func copyPass(pass closure: (inout NeHeCopyPass) throws(NeHeError) -> Void) throws(NeHeError) + { + var pass = NeHeCopyPass(self) + try closure(&pass) + try pass.submit() + } +} + +fileprivate extension NeHeContext +{ + struct ShaderProgramCreateInfo + { + var format: SDL_GPUShaderFormat + var vertexUniforms: UInt32 + var vertexStorage: UInt32 + var fragmentSamplers: UInt32 + } + + func loadShader(_ filePath: URL?, _ info: borrowing ShaderProgramCreateInfo, + _ type: SDL_GPUShaderStage, _ main: StaticString) throws(NeHeError) -> OpaquePointer + { + guard let path = filePath, let data = try? Data(contentsOf: path) else + { + throw .fatalError("Failed to open shader file") + } + return try loadShaderBlob(data, info, type, main) + } + + func loadShaderBlob(_ code: Data, _ info: ShaderProgramCreateInfo, + _ type: SDL_GPUShaderStage, _ main: StaticString) throws(NeHeError) -> OpaquePointer + { + guard let result = code.withContiguousStorageIfAvailable({ data in + main.withUTF8Buffer { $0.withMemoryRebound(to: Int8.self) { entryPoint in + var info = SDL_GPUShaderCreateInfo( + code_size: code.count, + code: data.baseAddress!, + entrypoint: entryPoint.baseAddress!, + format: info.format, + stage: type, + num_samplers: type == SDL_GPU_SHADERSTAGE_FRAGMENT ? info.fragmentSamplers : 0, + num_storage_textures: 0, + num_storage_buffers: type == SDL_GPU_SHADERSTAGE_VERTEX ? info.vertexStorage : 0, + num_uniform_buffers: type == SDL_GPU_SHADERSTAGE_VERTEX ? info.vertexUniforms : 0, + props: 0) + return SDL_CreateGPUShader(self.device, &info) + }} + }) else + { + throw .fatalError("Failed to convert shader blob to contiguous storage") + } + guard let shader = result else + { + throw .sdlError("SDL_CreateGPUShader", String(cString: SDL_GetError())) + } + return shader + } +} + +fileprivate extension URL +{ + func join(_ pathComponent: String, isDirectory dir: Bool? = nil) -> URL + { + if #available(macOS 13, *) + { + let hint: DirectoryHint = dir == nil ? .inferFromPath : (dir! ? .isDirectory : .notDirectory) + return self.appending(path: pathComponent, directoryHint: hint) + } + else + { + return dir == nil + ? self.appendingPathComponent(pathComponent) + : self.appendingPathComponent(pathComponent, isDirectory: dir!) + } + } +} diff --git a/src/swift/NeHe/NeHeCopyPass.swift b/src/swift/NeHe/NeHeCopyPass.swift new file mode 100644 index 0000000..a4b0a66 --- /dev/null +++ b/src/swift/NeHe/NeHeCopyPass.swift @@ -0,0 +1,270 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import SDLSwift +import Foundation + +public struct NeHeCopyPass: ~Copyable +{ + private var device: OpaquePointer + private var bundle: Bundle? + fileprivate var copies: [Copy] + + internal init(_ ctx: borrowing NeHeContext) + { + self.device = ctx.device + self.bundle = ctx.bundle + self.copies = .init() + } + + deinit + { + // Free transfer buffers + for copy in self.copies.reversed() + { + SDL_ReleaseGPUTransferBuffer(self.device, copy.xferBuffer) + } + } + + internal func submit() throws(NeHeError) + { + guard let cmd = SDL_AcquireGPUCommandBuffer(self.device) else + { + let message = String(cString: SDL_GetError()) + throw .sdlError("SDL_AcquireGPUCommandBuffer", message) + } + + // Begin the copy pass + let pass = SDL_BeginGPUCopyPass(cmd) + + // Upload data into the GPU buffer(s) + for copy in self.copies + { + var vtxSource = SDL_GPUTransferBufferLocation(transfer_buffer: copy.xferBuffer, offset: 0) + var vtxDestination = SDL_GPUBufferRegion(buffer: copy.buffer, offset: 0, size: copy.size) + SDL_UploadToGPUBuffer(pass, &vtxSource, &vtxDestination, false) + } + + // End & submit the copy pass + SDL_EndGPUCopyPass(pass) + SDL_SubmitGPUCommandBuffer(cmd) + } +} + +public extension NeHeCopyPass +{ + mutating func createBuffer(usage: SDL_GPUBufferUsageFlags, _ elements: ArraySlice) throws(NeHeError) -> OpaquePointer + { + // Create data buffer + let size = UInt32(MemoryLayout.stride * elements.count) + var info = SDL_GPUBufferCreateInfo(usage: usage, size: size, props: 0) + guard let buffer = SDL_CreateGPUBuffer(self.device, &info) else + { + throw .sdlError("SDL_CreateGPUBuffer", String(cString: SDL_GetError())) + } + + // Create transfer buffer + var xferInfo = SDL_GPUTransferBufferCreateInfo( + usage: SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + size: size, + props: 0) + guard let xferBuffer = SDL_CreateGPUTransferBuffer(self.device, &xferInfo) else + { + let message = String(cString: SDL_GetError()) + SDL_ReleaseGPUBuffer(self.device, buffer) + throw .sdlError("SDL_CreateGPUTransferBuffer", message) + } + + // Map transfer buffer and copy the data + guard let map = SDL_MapGPUTransferBuffer(self.device, xferBuffer, false) else + { + let message = String(cString: SDL_GetError()) + SDL_ReleaseGPUTransferBuffer(self.device, xferBuffer) + SDL_ReleaseGPUBuffer(self.device, buffer) + throw .sdlError("SDL_MapGPUTransferBuffer", message) + } + elements.withUnsafeBufferPointer + { + map.assumingMemoryBound(to: E.self).initialize(from: $0.baseAddress!, count: $0.count) + } + SDL_UnmapGPUTransferBuffer(self.device, xferBuffer) + + self.copies.append(.init( + buffer: buffer, + xferBuffer: xferBuffer, + size: size)) + return buffer + } + + mutating func createTextureFrom(bmpResource name: String, flip: Bool = false, genMipmaps: Bool = false) + throws(NeHeError) -> OpaquePointer + { + // Load image into a surface + guard let bundle = self.bundle, + let pathURL = bundle.url(forResource: name, withExtension: "bmp") + else + { + throw .fatalError("Failed to load BMP resource") + } + let path = if #available(macOS 13.0, *) { pathURL.path(percentEncoded: false) } else { pathURL.path } + guard let image = SDL_LoadBMP(path) else + { + throw .sdlError("SDL_LoadBMP", String(cString: SDL_GetError())) + } + defer { SDL_DestroySurface(image) } + + // Flip surface if requested + if flip + { + guard SDL_FlipSurface(image, SDL_FLIP_VERTICAL) else + { + throw .sdlError("SDL_FlipSurface", String(cString: SDL_GetError())) + } + } + + // Upload texture to GPU + return try self.createTextureFrom(surface: image, genMipmaps: genMipmaps) + } + + mutating func createTextureFrom(surface: UnsafeMutablePointer, + genMipmaps: Bool) throws(NeHeError) -> OpaquePointer + { + var info = SDL_GPUTextureCreateInfo() + info.type = SDL_GPU_TEXTURETYPE_2D + info.format = SDL_GPU_TEXTUREFORMAT_INVALID + info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER + info.width = UInt32(surface.pointee.w) + info.height = UInt32(surface.pointee.h) + info.layer_count_or_depth = 1 + info.num_levels = 1 + + let needsConvert: Bool + (needsConvert, info.format) = switch surface.pointee.format + { + case SDL_PIXELFORMAT_RGBA32: (false, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM) + case SDL_PIXELFORMAT_RGBA64: (false, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_UNORM) + case SDL_PIXELFORMAT_RGB565: (false, SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM) + case SDL_PIXELFORMAT_ARGB1555: (false, SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM) + case SDL_PIXELFORMAT_BGRA4444: (false, SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM) + case SDL_PIXELFORMAT_BGRA32: (false, SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM) + case SDL_PIXELFORMAT_RGBA64_FLOAT: (false, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT) + case SDL_PIXELFORMAT_RGBA128_FLOAT: (false, SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT) + default: (true, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM) + } + + func BYTESPERPIXEL(_ format: SDL_PixelFormat) -> UInt32 + { + let isFourCC = format.rawValue != 0 && (((format.rawValue >> 28) & 0xF) != 1) + return isFourCC + ? [ + SDL_PIXELFORMAT_YUY2, + SDL_PIXELFORMAT_UYVY, + SDL_PIXELFORMAT_YVYU, + SDL_PIXELFORMAT_P010 + ].contains(format) ? 2 : 1 + : format.rawValue & 0xFF + } + + let data: UnsafeRawBufferPointer + let conv: UnsafeMutablePointer? = nil + if needsConvert + { + // Convert pixel format if required + guard let conv = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ABGR8888) else + { + throw .sdlError("SDL_ConvertSurface", String(cString: SDL_GetError())) + } + let numPixels = Int(conv.pointee.w) * Int(conv.pointee.h) + data = .init(start: conv.pointee.pixels, count: Int(BYTESPERPIXEL(conv.pointee.format)) * numPixels) + } + else + { + let numPixels = Int(surface.pointee.w) * Int(surface.pointee.h) + data = .init(start: surface.pointee.pixels, count: Int(BYTESPERPIXEL(surface.pointee.format)) * numPixels) + } + defer { SDL_DestroySurface(conv) } + + if genMipmaps + { + info.usage |= SDL_GPU_TEXTUREUSAGE_COLOR_TARGET + // floor(log₂(max(𝑤,ℎ)) + 1 + info.num_levels = 31 - UInt32(max(info.width, info.height).leadingZeroBitCount) + 1 + } + + return try self.createTextureFrom(pixels: data, createInfo: &info, genMipmaps: genMipmaps) + } +} + +fileprivate extension NeHeCopyPass +{ + func createTextureFrom(pixels: UnsafeRawBufferPointer, + createInfo info: inout SDL_GPUTextureCreateInfo, genMipmaps: Bool) + throws(NeHeError) -> OpaquePointer + { + guard let texture = SDL_CreateGPUTexture(self.device, &info) else + { + throw .sdlError("SDL_CreateGPUTexture", String(cString: SDL_GetError())) + } + + // Create and copy image data to a transfer buffer + var xferInfo = SDL_GPUTransferBufferCreateInfo( + usage: SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + size: UInt32(pixels.count), + props: 0) + guard let xferBuffer = SDL_CreateGPUTransferBuffer(self.device, &xferInfo) else + { + SDL_ReleaseGPUTexture(self.device, texture) + throw .sdlError("SDL_CreateGPUTransferBuffer", String(cString: SDL_GetError())) + } + defer { SDL_ReleaseGPUTransferBuffer(self.device, xferBuffer) } + + guard let map = SDL_MapGPUTransferBuffer(self.device, xferBuffer, false) else + { + SDL_ReleaseGPUTexture(self.device, texture) + throw .sdlError("SDL_MapGPUTransferBuffer", String(cString: SDL_GetError())) + } + map.initializeMemory(as: UInt8.self, + from: pixels.withMemoryRebound(to: UInt8.self, \.baseAddress!), + count: pixels.count) + SDL_UnmapGPUTransferBuffer(self.device, xferBuffer) + + // Upload the transfer data to the GPU resources + guard let cmd = SDL_AcquireGPUCommandBuffer(self.device) else + { + SDL_ReleaseGPUTexture(self.device, texture) + throw .sdlError("SDL_AcquireGPUCommandBuffer", String(cString: SDL_GetError())) + } + + let pass = SDL_BeginGPUCopyPass(cmd) + var source = SDL_GPUTextureTransferInfo() + source.transfer_buffer = xferBuffer + source.offset = 0 + var destination = SDL_GPUTextureRegion() + destination.texture = texture + destination.w = info.width + destination.h = info.height + destination.d = info.layer_count_or_depth + SDL_UploadToGPUTexture(pass, &source, &destination, false) + SDL_EndGPUCopyPass(pass) + + if genMipmaps + { + SDL_GenerateMipmapsForGPUTexture(cmd, texture) + } + + SDL_SubmitGPUCommandBuffer(cmd) + return texture + } +} + +fileprivate extension NeHeCopyPass +{ + struct Copy + { + var buffer: OpaquePointer + var xferBuffer: OpaquePointer + var size: UInt32 + } +} diff --git a/src/swift/NeHe/NeHeError.swift b/src/swift/NeHe/NeHeError.swift new file mode 100644 index 0000000..0229716 --- /dev/null +++ b/src/swift/NeHe/NeHeError.swift @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import Foundation + +public enum NeHeError: Error +{ + case fatalError(StaticString) + case sdlError(StaticString, String) +} + +extension NeHeError: LocalizedError +{ + public var errorDescription: String? + { + switch self + { + case .fatalError(let why): "\(why)" + case .sdlError(let fname, let message): "\(fname): \(message)" + } + } +} diff --git a/src/swift/NeHe/Size.swift b/src/swift/NeHe/Size.swift new file mode 100644 index 0000000..c04f5eb --- /dev/null +++ b/src/swift/NeHe/Size.swift @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +public struct Size: Equatable +{ + public var width: T, height: T + + public init(width: T, height: T) + { + self.width = width + self.height = height + } + + @inline(__always) public init(_ w: T, _ h: T) { self.init(width: w, height: h) } +} + +public extension Size +{ + @inline(__always) static var zero: Self { Self(0, 0) } +} + +public extension Size where T: BinaryInteger +{ + init(_ other: Size) + { + self.init(T(other.width), T(other.height)) + } +}