diff --git a/Sources/Voxelotl/Application.swift b/Sources/Voxelotl/Application.swift index 6d51cca..05e5faa 100644 --- a/Sources/Voxelotl/Application.swift +++ b/Sources/Voxelotl/Application.swift @@ -31,15 +31,15 @@ public class Application { if cfg.flags.contains(.highDPI) { windowFlags |= SDL_WindowFlags(SDL_WINDOW_HIGH_PIXEL_DENSITY) } - window = SDL_CreateWindow(cfg.title, cfg.width, cfg.height, windowFlags) + window = SDL_CreateWindow(cfg.title, cfg.frame.w, cfg.frame.h, windowFlags) guard window != nil else { printErr("SDL_CreateWindow() error: \(String(cString: SDL_GetError()))") return .exitFailure } // Get window metrics - var backBufferWidth: Int32 = 0, backBufferHeight: Int32 = 0 - guard SDL_GetWindowSizeInPixels(window, &backBufferWidth, &backBufferHeight) >= 0 else { + var backBuffer = Size.zero + guard SDL_GetWindowSizeInPixels(window, &backBuffer.w, &backBuffer.h) >= 0 else { printErr("SDL_GetWindowSizeInPixels() error: \(String(cString: SDL_GetError()))") return .exitFailure } @@ -49,7 +49,7 @@ public class Application { do { let layer = unsafeBitCast(SDL_Metal_GetLayer(view), to: CAMetalLayer.self) layer.displaySyncEnabled = cfg.vsyncMode == .off ? false : true - self.renderer = try Renderer(layer: layer, size: .init(Int(backBufferWidth), Int(backBufferHeight))) + self.renderer = try Renderer(layer: layer, size: Size(backBuffer)) } catch RendererError.initFailure(let message) { printErr("Renderer init error: \(message)") return .exitFailure @@ -122,9 +122,9 @@ public class Application { del.update(gameTime) do { - try renderer!.beginFrame() - del.draw(renderer!, gameTime) - renderer!.endFrame() + try renderer!.newFrame { + del.draw($0, gameTime) + } } catch RendererError.drawFailure(let message) { printErr("Renderer draw error: \(message)") return .exitFailure @@ -182,15 +182,13 @@ public struct ApplicationConfiguration { case adaptive } - let width: Int32 - let height: Int32 + let frame: Size let title: String let flags: Flags let vsyncMode: VSyncMode - public init(width: Int32, height: Int32, title: String, flags: Flags, vsyncMode: VSyncMode) { - self.width = width - self.height = height + public init(frame: Size, title: String, flags: Flags, vsyncMode: VSyncMode) { + self.frame = frame self.title = title self.flags = flags self.vsyncMode = vsyncMode diff --git a/Sources/Voxelotl/Rectangle.swift b/Sources/Voxelotl/Rectangle.swift index 481ec32..cc5dabb 100644 --- a/Sources/Voxelotl/Rectangle.swift +++ b/Sources/Voxelotl/Rectangle.swift @@ -42,6 +42,10 @@ public struct Size: Equatable { extension Size where T: BinaryInteger { static var one: Self { .init(T(1), T(1)) } + + init(_ other: Size) where O: BinaryInteger { + self.init(T(other.w), T(other.h)) + } } struct Rect: Equatable { diff --git a/Sources/Voxelotl/Renderer.swift b/Sources/Voxelotl/Renderer.swift index e56fc4e..60db3a2 100644 --- a/Sources/Voxelotl/Renderer.swift +++ b/Sources/Voxelotl/Renderer.swift @@ -55,9 +55,7 @@ public class Renderer { private var depthStencilState: MTLDepthStencilState private var depthTextures: [MTLTexture] - private var _commandBuf: MTLCommandBuffer! - private var _encoder: MTLRenderCommandEncoder! - private var _rt: (any CAMetalDrawable)! + private var _encoder: MTLRenderCommandEncoder! = nil private var vtxBuffer: MTLBuffer, idxBuffer: MTLBuffer private var defaultTexture: MTLTexture @@ -201,71 +199,75 @@ public class Renderer { } static func loadTexture(_ device: MTLDevice, _ queue: MTLCommandQueue, image2D image: Image2D) throws -> MTLTexture { - let texDesc = MTLTextureDescriptor() - texDesc.width = image.width - texDesc.height = image.height - texDesc.pixelFormat = .rgba8Unorm_srgb - texDesc.textureType = .type2D - texDesc.storageMode = .private - texDesc.usage = .shaderRead - guard let newTexture = device.makeTexture(descriptor: texDesc) else { - throw RendererError.loadFailure("Failed to create texture descriptor") + try autoreleasepool { + let texDesc = MTLTextureDescriptor() + texDesc.width = image.width + texDesc.height = image.height + texDesc.pixelFormat = .rgba8Unorm_srgb + texDesc.textureType = .type2D + texDesc.storageMode = .private + texDesc.usage = .shaderRead + guard let newTexture = device.makeTexture(descriptor: texDesc) else { + throw RendererError.loadFailure("Failed to create texture descriptor") + } + + guard let texData = image.data.withUnsafeBytes({ bytes in + device.makeBuffer(bytes: bytes.baseAddress!, length: bytes.count, options: [ .storageModeShared ]) + }) else { + throw RendererError.loadFailure("Failed to create shared texture data buffer") + } + + guard let cmdBuffer = queue.makeCommandBuffer(), + let blitEncoder = cmdBuffer.makeBlitCommandEncoder() + else { + throw RendererError.loadFailure("Failed to create blit command encoder") + } + + blitEncoder.copy( + from: texData, + sourceOffset: 0, + sourceBytesPerRow: image.stride, + sourceBytesPerImage: image.stride * image.height, + sourceSize: .init(width: image.width, height: image.height, depth: 1), + + to: newTexture, + destinationSlice: 0, + destinationLevel: 0, + destinationOrigin: .init(x: 0, y: 0, z: 0)) + blitEncoder.endEncoding() + + cmdBuffer.addCompletedHandler { _ in + //FIXME: look into if this needs to be synchronised + //printErr("Texture was added?") + } + cmdBuffer.commit() + + return newTexture } - - guard let texData = image.data.withUnsafeBytes({ bytes in - device.makeBuffer(bytes: bytes.baseAddress!, length: bytes.count, options: [ .storageModeShared ]) - }) else { - throw RendererError.loadFailure("Failed to create shared texture data buffer") - } - - guard let cmdBuffer = queue.makeCommandBuffer(), - let blitEncoder = cmdBuffer.makeBlitCommandEncoder() - else { - throw RendererError.loadFailure("Failed to create blit command encoder") - } - - blitEncoder.copy( - from: texData, - sourceOffset: 0, - sourceBytesPerRow: image.stride, - sourceBytesPerImage: image.stride * image.height, - sourceSize: .init(width: image.width, height: image.height, depth: 1), - - to: newTexture, - destinationSlice: 0, - destinationLevel: 0, - destinationOrigin: .init(x: 0, y: 0, z: 0)) - blitEncoder.endEncoding() - - cmdBuffer.addCompletedHandler { _ in - //FIXME: look into if this needs to be synchronised - //printErr("Texture was added?") - } - cmdBuffer.commit() - - return newTexture } private static func createDepthTexture(_ device: MTLDevice, _ size: Size, format: MTLPixelFormat ) -> MTLTexture? { - let texDescriptor = MTLTextureDescriptor.texture2DDescriptor( - pixelFormat: format, - width: size.w, - height: size.h, - mipmapped: false) - texDescriptor.depth = 1 - texDescriptor.sampleCount = 1 - texDescriptor.usage = [ .renderTarget, .shaderRead ] -#if !NDEBUG - texDescriptor.storageMode = .private -#else - texDescriptor.storageMode = .memoryless -#endif + autoreleasepool { + let texDescriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: format, + width: size.w, + height: size.h, + mipmapped: false) + texDescriptor.depth = 1 + texDescriptor.sampleCount = 1 + texDescriptor.usage = [ .renderTarget, .shaderRead ] + #if !NDEBUG + texDescriptor.storageMode = .private + #else + texDescriptor.storageMode = .memoryless + #endif - guard let depthStencilTexture = device.makeTexture(descriptor: texDescriptor) else { return nil } - depthStencilTexture.label = "Depth buffer" + guard let depthStencilTexture = device.makeTexture(descriptor: texDescriptor) else { return nil } + depthStencilTexture.label = "Depth buffer" - return depthStencilTexture + return depthStencilTexture + } } static func makeViewport(rect: Rect, znear: Double = 0.0, zfar: Double = 1.0) -> MTLViewport { @@ -288,41 +290,56 @@ public class Renderer { self._aspectRatio = Float(self.backBufferSize.w) / Float(self.backBufferSize.h) } - func beginFrame() throws { - // Lock the semaphore here if too many frames are "in flight" - _ = inFlightSemaphore.wait(timeout: .distantFuture) + func newFrame(_ frameFunc: (Renderer) -> Void) throws { + try autoreleasepool { + guard let rt = layer.nextDrawable() else { + throw RendererError.drawFailure("Failed to get next drawable render target") + } - guard let rt = layer.nextDrawable() else { - throw RendererError.drawFailure("Failed to get next drawable render target") + passDescription.colorAttachments[0].texture = rt.texture + passDescription.depthAttachment.texture = self.depthTextures[self.currentFrame] + + // Lock the semaphore here if too many frames are "in flight" + _ = inFlightSemaphore.wait(timeout: .distantFuture) + + guard let commandBuf: MTLCommandBuffer = queue.makeCommandBuffer() else { + throw RendererError.drawFailure("Failed to make command buffer from queue") + } + commandBuf.addCompletedHandler { _ in + self.inFlightSemaphore.signal() + } + + guard let encoder = commandBuf.makeRenderCommandEncoder(descriptor: passDescription) else { + throw RendererError.drawFailure("Failed to make render encoder from command buffer") + } + + encoder.setCullMode(.back) + encoder.setFrontFacing(.counterClockwise) // OpenGL default + encoder.setViewport(Self.makeViewport(rect: self.frame)) + encoder.setRenderPipelineState(pso) + encoder.setDepthStencilState(depthStencilState) + encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0) + encoder.setVertexBuffer(vtxBuffer, + offset: 0, + index: ShaderInputIdx.vertices.rawValue) + + self._encoder = encoder + frameFunc(self) + self._encoder = nil + + encoder.endEncoding() + commandBuf.present(rt) + commandBuf.commit() + + self.currentFrame &+= 1 + if self.currentFrame == numFramesInFlight { + self.currentFrame = 0 + } } - self._rt = rt - - passDescription.colorAttachments[0].texture = self._rt.texture - passDescription.depthAttachment.texture = self.depthTextures[self.currentFrame] - - guard let commandBuf: MTLCommandBuffer = queue.makeCommandBuffer() else { - throw RendererError.drawFailure("Failed to make command buffer from queue") - } - self._commandBuf = commandBuf - self._commandBuf.addCompletedHandler { _ in - self.inFlightSemaphore.signal() - } - - guard let encoder = self._commandBuf.makeRenderCommandEncoder(descriptor: passDescription) else { - throw RendererError.drawFailure("Failed to make render encoder from command buffer") - } - self._encoder = encoder - - self._encoder.setCullMode(.back) - self._encoder.setFrontFacing(.counterClockwise) // OpenGL default - self._encoder.setViewport(Self.makeViewport(rect: self.frame)) - self._encoder.setRenderPipelineState(pso) - self._encoder.setDepthStencilState(depthStencilState) - - self._encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0) } func batch(instances: [Instance], camera: Camera) { + assert(self._encoder != nil, "batch can't be called outside of a frame being rendered") assert(instances.count < 52) var uniforms = ShaderUniforms(projView: camera.viewProjection) @@ -339,10 +356,6 @@ public class Renderer { UInt8(instance.color.w * 0xFF))) } - self._encoder.setVertexBuffer(vtxBuffer, - offset: 0, - index: ShaderInputIdx.vertices.rawValue) - // Ideal as long as our uniforms total 4 KB or less self._encoder.setVertexBytes(instances, length: instances.count * MemoryLayout.stride, @@ -359,21 +372,6 @@ public class Renderer { indexBufferOffset: 0, instanceCount: instances.count) } - - func endFrame() { - self._encoder.endEncoding() - self._commandBuf.present(self._rt) - self._commandBuf.commit() - - self._rt = nil - self._encoder = nil - self._commandBuf = nil - - self.currentFrame &+= 1 - if self.currentFrame == numFramesInFlight { - self.currentFrame = 0 - } - } } enum RendererError: Error { diff --git a/Sources/Voxelotl/main.swift b/Sources/Voxelotl/main.swift index ee43e88..a6c1ab5 100644 --- a/Sources/Voxelotl/main.swift +++ b/Sources/Voxelotl/main.swift @@ -1,13 +1,9 @@ import Darwin -var rect = Rect(origin: .init(0, 0), size: .init(32, 32)) -rect.origin += Point(10, 10) - let app = Application( delegate: Game(), configuration: ApplicationConfiguration( - width: 1280, - height: 720, + frame: Size(1280, 720), title: "Voxelotl Demo", flags: [ .resizable, .highDPI ], vsyncMode: .on(interval: 1)))