From 61f2c73b77cdb0d299223f75504303658be3652b Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Fri, 13 Jun 2025 21:08:34 +1000 Subject: [PATCH] swift: Implement lesson09 --- Package.swift | 4 + src/swift/Lesson9/lesson9.swift | 331 ++++++++++++++++++++++++++++++++ src/swift/NeHe/NeHeRandom.swift | 25 +++ 3 files changed, 360 insertions(+) create mode 100644 src/swift/Lesson9/lesson9.swift create mode 100644 src/swift/NeHe/NeHeRandom.swift diff --git a/Package.swift b/Package.swift index 6b681df..526c6af 100644 --- a/Package.swift +++ b/Package.swift @@ -13,6 +13,7 @@ let package = Package( .executable(name: "Lesson6", targets: [ "Lesson6" ]), .executable(name: "Lesson7", targets: [ "Lesson7" ]), .executable(name: "Lesson8", targets: [ "Lesson8" ]), + .executable(name: "Lesson9", targets: [ "Lesson9" ]), ], dependencies: [ .package(url: "https://github.com/GayPizzaSpecifications/SDL3Swift.git", branch: "main"), @@ -42,5 +43,8 @@ let package = Package( .process("../../../data/shaders/lesson6.metallib"), .process("../../../data/shaders/lesson7.metallib"), .process("../../../data/Glass.bmp") ]), + .executableTarget(name: "Lesson9", dependencies: [ "NeHe" ], path: "src/swift/Lesson9", resources: [ + .process("../../../data/shaders/lesson9.metallib"), + .process("../../../data/Star.bmp") ]), ], ) diff --git a/src/swift/Lesson9/lesson9.swift b/src/swift/Lesson9/lesson9.swift new file mode 100644 index 0000000..f259ac8 --- /dev/null +++ b/src/swift/Lesson9/lesson9.swift @@ -0,0 +1,331 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +import Foundation +import SDLSwift +import NeHe +import simd + +struct Lesson8: AppDelegate +{ + struct Vertex + { + let position: SIMD3, texcoord: SIMD2 + + init(_ position: SIMD3, _ texcoord: SIMD2) + { + self.position = position + self.texcoord = texcoord + } + } + + struct Instance + { + let model: matrix_float4x4 + let color: SIMD4 + } + + struct Star + { + var angle: Float + var distance: Float + var color: SIMD3 + } + + static let vertices = + [ + Vertex(.init(-1.0, -1.0, 0.0), .init(0.0, 0.0)), + Vertex(.init( 1.0, -1.0, 0.0), .init(1.0, 0.0)), + Vertex(.init( 1.0, 1.0, 0.0), .init(1.0, 1.0)), + Vertex(.init(-1.0, 1.0, 0.0), .init(0.0, 1.0)), + ] + + static let indices: [UInt16] = + [ + 0, 1, 2, + 2, 3, 0, + ] + + var pso: OpaquePointer? = nil + var vtxBuffer: OpaquePointer? = nil + var idxBuffer: OpaquePointer? = nil + var instanceBuffer: OpaquePointer? = nil + var instanceXferBuffer: OpaquePointer? = nil + var sampler: OpaquePointer? = nil + var texture: OpaquePointer? = nil + + var projection: matrix_float4x4 = .init(1.0) + + var twinkle = false + var stars: [Star] = [] + + var zoom: Float = -15.0 + var tilt: Float = 90.0 + var spin: Float = 0.0 + + var random = NeHeRandom() + + mutating func `init`(ctx: inout NeHeContext) throws(NeHeError) + { + let (vertexShader, fragmentShader) = try ctx.loadShaders(name: "lesson9", + vertexUniforms: 1, vertexStorage: 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)!)), + ] + + 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 colorTargets = [ SDL_GPUColorTargetDescription() ] + colorTargets[0].format = SDL_GetGPUSwapchainTextureFormat(ctx.device, ctx.window) + colorTargets[0].blend_state.enable_blend = true + colorTargets[0].blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD + colorTargets[0].blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD + colorTargets[0].blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA + colorTargets[0].blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE + colorTargets[0].blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA + colorTargets[0].blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE + + var targetInfo = SDL_GPUGraphicsPipelineTargetInfo() + targetInfo.color_target_descriptions = colorTargets.withUnsafeBufferPointer(\.baseAddress!) + targetInfo.num_color_targets = UInt32(colorTargets.count) + + var psoInfo = 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, &psoInfo) 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: "Star", flip: true, genMipmaps: false) + self.vtxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_VERTEX, Self.vertices[...]) + self.idxBuffer = try pass.createBuffer(usage: SDL_GPU_BUFFERUSAGE_INDEX, Self.indices[...]) + } + + let numStars = 50 + + // Create GPU side buffer for star instances + let instanceBufferSize = UInt32(MemoryLayout.stride * 2 * numStars) + var instanceInfo = SDL_GPUBufferCreateInfo( + usage: SDL_GPU_BUFFERUSAGE_GRAPHICS_STORAGE_READ, + size: instanceBufferSize, + props: 0) + guard let instanceBuffer = SDL_CreateGPUBuffer(ctx.device, &instanceInfo) else + { + throw .sdlError("SDL_CreateGPUBuffer", String(cString: SDL_GetError())) + } + self.instanceBuffer = instanceBuffer + + // Create CPU side buffer for star instances (to upload to GPU) + var instanceXferInfo = SDL_GPUTransferBufferCreateInfo( + usage: SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, + size: instanceBufferSize, + props: 0) + guard let instanceXferBuffer = SDL_CreateGPUTransferBuffer(ctx.device, &instanceXferInfo) else + { + throw .sdlError("SDL_CreateGPUTransferBuffer", String(cString: SDL_GetError())) + } + self.instanceXferBuffer = instanceXferBuffer + + // Initialise stars + self.stars = (0..) + { + 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) + { + // Animate stars + let numStars = self.stars.count + let numInstances = self.twinkle ? 2 * numStars : numStars + let map = SDL_MapGPUTransferBuffer(ctx.device, self.instanceXferBuffer, true)! + map.withMemoryRebound(to: Instance.self, capacity: numInstances) + { instances in + var instanceIDX = 0 + for (starIDX, star) in self.stars.enumerated() + { + var model = matrix_float4x4.translation(.init(0.0, 0.0, zoom)) + model.rotate(angle: tilt, axis: .init(1.0, 0.0, 0.0)) + model.rotate(angle: star.angle, axis: .init(0.0, 1.0, 0.0)) + model.translate(.init(star.distance, 0.0, 0.0)) + model.rotate(angle: -star.angle, axis: .init(0.0, 1.0, 0.0)) + model.rotate(angle: -tilt, axis: .init(1.0, 0.0, 0.0)) + + if self.twinkle + { + let twinkleColor = stars[numStars - starIDX - 1].color + instances[instanceIDX] = Instance(model: model, + color: SIMD4(SIMD3(twinkleColor) / 255.0, 1.0)) + instanceIDX += 1 + } + + model.rotate(angle: self.spin, axis: .init(0.0, 0.0, 1.0)) + instances[instanceIDX] = Instance(model: model, color: SIMD4(SIMD3(star.color) / 255.0, 1.0)) + instanceIDX += 1 + + self.spin += 0.01 + + // Update star + var newStar = star + newStar.angle += Float(starIDX) / Float(numStars) + newStar.distance -= 0.01 + if newStar.distance < 0.0 + { + newStar.distance += 5.0 + newStar.color = SIMD3(newStar.color.indices.map { _ in + UInt8(truncatingIfNeeded: self.random.next() % 256) + }) + } + self.stars[starIDX] = newStar + } + } + SDL_UnmapGPUTransferBuffer(ctx.device, self.instanceXferBuffer) + + // Upload instances buffer to the GPU + let copyPass = SDL_BeginGPUCopyPass(cmd) + var source = SDL_GPUTransferBufferLocation( + transfer_buffer: self.instanceXferBuffer, + offset: 0) + var destination = SDL_GPUBufferRegion( + buffer: self.instanceBuffer, + offset: 0, + size: UInt32(MemoryLayout.stride * numInstances)) + SDL_UploadToGPUBuffer(copyPass, &source, &destination, true) + SDL_EndGPUCopyPass(copyPass) + + // Begin pass & bind pipeline state + 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 renderPass = SDL_BeginGPURenderPass(cmd, &colorInfo, 1, nil) + SDL_BindGPUGraphicsPipeline(renderPass, self.pso) + + // Bind particle texture + var textureBinding = SDL_GPUTextureSamplerBinding(texture: self.texture, sampler: self.sampler) + SDL_BindGPUFragmentSamplers(renderPass, 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(renderPass, 0, + vtxBindings.withUnsafeBufferPointer(\.baseAddress!), UInt32(vtxBindings.count)) + SDL_BindGPUIndexBuffer(renderPass, &idxBinding, SDL_GPU_INDEXELEMENTSIZE_16BIT) + + // Bind instance storage buffer + SDL_BindGPUVertexStorageBuffers(renderPass, 0, &self.instanceBuffer, 1) + + // Push shader uniforms + SDL_PushGPUVertexUniformData(cmd, 0, &self.projection, UInt32(MemoryLayout.size)) + + // Draw star instances + SDL_DrawGPUIndexedPrimitives(renderPass, UInt32(Self.indices.count), UInt32(numInstances), 0, 0, 0) + + SDL_EndGPURenderPass(renderPass) + + let keys = SDL_GetKeyboardState(nil)! + + if keys[Int(SDL_SCANCODE_UP.rawValue)] { tilt -= 0.5 } + if keys[Int(SDL_SCANCODE_DOWN.rawValue)] { tilt += 0.5 } + if keys[Int(SDL_SCANCODE_PAGEUP.rawValue)] { zoom -= 0.2 } + if keys[Int(SDL_SCANCODE_PAGEDOWN.rawValue)] { zoom += 0.2 } + } + + mutating func key(ctx: inout NeHeContext, key: SDL_Keycode, down: Bool, repeat: Bool) + { + guard down && !`repeat` else { return } + switch key + { + case SDLK_T: + self.twinkle = !self.twinkle + default: + break + } + } +} + +@main struct Program: AppRunner +{ + typealias Delegate = Lesson8 + static let config = AppConfig( + title: "NeHe's Animated Blended Textures Tutorial", + width: 640, + height: 480, + bundle: Bundle.module) +} diff --git a/src/swift/NeHe/NeHeRandom.swift b/src/swift/NeHe/NeHeRandom.swift new file mode 100644 index 0000000..f21a751 --- /dev/null +++ b/src/swift/NeHe/NeHeRandom.swift @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: (C) 2025 a dinosaur + * SPDX-License-Identifier: Zlib + */ + +public struct NeHeRandom: Equatable +{ + var state: UInt32 + + public init() + { + self.state = 1 + } + + public init(seed: UInt32) + { + self.state = seed + } + + public mutating func next() -> Int32 + { + self.state = self.state &* 214013 &+ 2531011 + return Int32(bitPattern: (self.state >> 16) & 0x7FFF) // (s / 65536) % 32768 + } +}