From 2b2539d7dfd6f30752e3e95dfcfc77214cae192b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 18 Nov 2018 21:15:20 -0700 Subject: [PATCH] Implement progress tracking --- Terrain2/Algorithms.swift | 56 ++++++++++++++---------- Terrain2/GameViewController.swift | 32 +++++++++++++- Terrain2/Renderer.swift | 6 ++- Terrain2/Shaders/TerrainAlgorithms.metal | 5 +++ Terrain2/Terrain.swift | 43 ++++++++++++++++++ 5 files changed, 116 insertions(+), 26 deletions(-) diff --git a/Terrain2/Algorithms.swift b/Terrain2/Algorithms.swift index 451c4fc..1e0b64f 100644 --- a/Terrain2/Algorithms.swift +++ b/Terrain2/Algorithms.swift @@ -25,7 +25,7 @@ protocol TerrainGenerator { func updateUniforms() func encode(in encoder: MTLComputeCommandEncoder) - func render(completion: @escaping () -> Void) + func render(progress: Progress) -> [Float] } class Kernel { @@ -99,7 +99,9 @@ class ZeroAlgorithm: Kernel, TerrainGenerator { func updateUniforms() { } - func render(completion: @escaping () -> Void) { } + func render(progress: Progress) -> [Float] { + return [] + } } /// Randomly generate heights that are independent of all others. @@ -133,7 +135,9 @@ class RandomAlgorithm: Kernel, TerrainGenerator { RandomAlgorithmUniforms_refreshRandoms(uniforms) } - func render(completion: @escaping () -> Void) { } + func render(progress: Progress) -> [Float] { + return [] + } } /// Implementation of the Diamond-Squares algorithm. @@ -224,12 +228,19 @@ public class DiamondSquareGenerator: TerrainGenerator { ] } - func breadthFirstSearch(visit: (Box) -> (Void)) { + func breadthFirstSearch(progress: Progress, visit: (Box) -> (Void)) { var queue = Queue() + queue.enqueue(item: self) + progress.totalUnitCount += 1 + while let box = queue.dequeue() { visit(box) - queue.enqueue(items: box.subdivisions) + progress.completedUnitCount += 1 + + let subdivisions = box.subdivisions + queue.enqueue(items: subdivisions) + progress.totalUnitCount += Int64(subdivisions.count) } } } @@ -237,8 +248,6 @@ public class DiamondSquareGenerator: TerrainGenerator { struct Algorithm { let grid: Box - private let queue = DispatchQueue(label: "Diamond-Square Algorithm Queue") - var roughness: Float = 1.0 { didSet { randomRange = -roughness...roughness @@ -253,14 +262,9 @@ public class DiamondSquareGenerator: TerrainGenerator { } /// Run the algorithm and return the genreated height map. - func render(completion: @escaping ([Float]) -> Void) { - queue.async { - let heightMap = self.queue_render() - completion(heightMap) - } - } + func render(progress: Progress) -> [Float] { + let renderProgress = Progress(totalUnitCount: 1, parent: progress, pendingUnitCount: 1) - func queue_render() -> [Float] { os_signpost(.begin, log: Log, name: "DiamondSquare.render") var heightMap = [Float](repeating: 0, count: grid.size.w * grid.size.h) @@ -270,8 +274,9 @@ public class DiamondSquareGenerator: TerrainGenerator { let idx = convert(pointToIndex: p) heightMap[idx] = Float.random(in: randomRange) } + renderProgress.completedUnitCount += 1 - grid.breadthFirstSearch { (box: Box) in + grid.breadthFirstSearch(progress: renderProgress) { (box: Box) in // 1. Diamond step. Find the midpoint of the square defined by `box` and set its value. let midpoint = box.midpoint let cornerValues = box.corners.map { (pt: Point) -> Float in @@ -366,15 +371,20 @@ public class DiamondSquareGenerator: TerrainGenerator { algorithm = Algorithm(grid: Box(origin: Point(), size: Size(w: DiamondSquareGenerator.textureSize.width, h: DiamondSquareGenerator.textureSize.height))) } - func render(completion: @escaping () -> Void) { - algorithm.render { (heightMap: [Float]) in - let region = MTLRegion(origin: MTLOrigin(), size: DiamondSquareGenerator.textureSize) - let newActiveTexture = (self.activeTexture + 1) % self.textures.count - self.textures[newActiveTexture].replace(region: region, mipmapLevel: 0, withBytes: heightMap, bytesPerRow: MemoryLayout.stride * DiamondSquareGenerator.textureSize.width) - self.activeTexture = newActiveTexture + func render(progress: Progress) -> [Float] { + let algProgress = Progress(totalUnitCount: 2, parent: progress, pendingUnitCount: 2) - completion() - } + let heights = algorithm.render(progress: algProgress) + algProgress.completedUnitCount += 1 + + // Swap the active texture to the new one. Copy the height map into the new texture. + let region = MTLRegion(origin: MTLOrigin(), size: DiamondSquareGenerator.textureSize) + let newActiveTexture = (self.activeTexture + 1) % self.textures.count + self.textures[newActiveTexture].replace(region: region, mipmapLevel: 0, withBytes: heights, bytesPerRow: MemoryLayout.stride * DiamondSquareGenerator.textureSize.width) + self.activeTexture = newActiveTexture + algProgress.completedUnitCount += 1 + + return heights } // MARK: Algorithm diff --git a/Terrain2/GameViewController.swift b/Terrain2/GameViewController.swift index 1f51219..61d5b2b 100644 --- a/Terrain2/GameViewController.swift +++ b/Terrain2/GameViewController.swift @@ -14,6 +14,9 @@ class GameViewController: NSViewController { var renderer: Renderer! var mtkView: MTKView! + var progressIndicator: NSProgressIndicator! + + private var progressObservation: NSKeyValueObservation? override func viewDidLoad() { super.viewDidLoad() @@ -41,6 +44,17 @@ class GameViewController: NSViewController { renderer.mtkView(mtkView, drawableSizeWillChange: mtkView.drawableSize) mtkView.delegate = renderer + + progressIndicator = NSProgressIndicator() + progressIndicator.isIndeterminate = false + progressIndicator.minValue = 0.0 + progressIndicator.maxValue = 1.0 + self.view.addSubview(progressIndicator) + progressIndicator.translatesAutoresizingMaskIntoConstraints = false + progressIndicator.widthAnchor.constraint(equalToConstant: 200.0).isActive = true + progressIndicator.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true + progressIndicator.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -40.0).isActive = true + progressIndicator.isHidden = true } override func viewWillAppear() { @@ -55,7 +69,23 @@ class GameViewController: NSViewController { override func keyDown(with event: NSEvent) { switch event.charactersIgnoringModifiers { case .some("n"): - renderer.scheduleAlgorithmIteration() + if let progress = renderer.scheduleAlgorithmIteration() { + progressIndicator.isHidden = false + progressObservation = progress.observe(\.fractionCompleted) { [weak self] (progress: Progress, change: NSKeyValueObservedChange) in + print("Observing progress change: \(progress.fractionCompleted)") + DispatchQueue.main.async { + guard let strongSelf = self else { + return + } + strongSelf.progressIndicator.doubleValue = progress.fractionCompleted + if progress.isFinished { + // TODO: Delay this. + strongSelf.progressIndicator.isHidden = true + strongSelf.progressObservation = nil + } + } + } + } case .some("z"): renderer.drawLines = !renderer.drawLines default: diff --git a/Terrain2/Renderer.swift b/Terrain2/Renderer.swift index 4e84916..06de93d 100644 --- a/Terrain2/Renderer.swift +++ b/Terrain2/Renderer.swift @@ -166,16 +166,18 @@ class Renderer: NSObject, MTKViewDelegate { } - func scheduleAlgorithmIteration() { + func scheduleAlgorithmIteration() -> Progress? { + var progress: Progress? = nil regenerationSemaphore.wait() if !terrain.generator.needsGPU { print("Rendering terrain...") - self.terrain.generator.render { + progress = self.terrain.generate { print("Rendering terrain...complete!") self.didUpdateTerrain = true } } regenerationSemaphore.signal() + return progress } private func updateDynamicBufferState() { diff --git a/Terrain2/Shaders/TerrainAlgorithms.metal b/Terrain2/Shaders/TerrainAlgorithms.metal index 813a4f6..f217aa9 100644 --- a/Terrain2/Shaders/TerrainAlgorithms.metal +++ b/Terrain2/Shaders/TerrainAlgorithms.metal @@ -73,6 +73,11 @@ kernel void updateGeometryNormals(constant float3 *vertexes [[buffer(GeneratorBu normals[tid] = normal; } +kernel void updateGeometryVertexNormals() +{ + +} + #pragma mark - ZeroGenerator kernel void zeroKernel(texture2d outTexture [[texture(GeneratorTextureIndexOut)]], diff --git a/Terrain2/Terrain.swift b/Terrain2/Terrain.swift index f917e6f..0b464b0 100644 --- a/Terrain2/Terrain.swift +++ b/Terrain2/Terrain.swift @@ -69,6 +69,20 @@ class Terrain: NSObject { return try MTKMesh(mesh:plane, device:device) } + + class func computePipeline(withFunctionNamed name: String, device: MTLDevice, library: MTLLibrary) throws -> MTLComputePipelineState { + guard let function = library.makeFunction(name: name) else { + throw RendererError.badComputeFunction + } + let pipeline = try device.makeComputePipelineState(function: function) + return pipeline + } + + private let generatorQueue = DispatchQueue(label: "Terrain Generation Queue") + + private let updateHeightsPipeline: MTLComputePipelineState + private let updateSurfaceNormalsPipeline: MTLComputePipelineState + private let updateVertexNormalsPipeline: MTLComputePipelineState let dimensions: float2 let segments: uint2 @@ -96,6 +110,35 @@ class Terrain: NSObject { (gen as DiamondSquareGenerator).roughness = 0.075 generator = gen + do { + updateHeightsPipeline = try Terrain.computePipeline(withFunctionNamed: "updateGeometryHeights", device: device, library: library) + updateSurfaceNormalsPipeline = try Terrain.computePipeline(withFunctionNamed: "updateGeometryNormals", device: device, library: library) + updateVertexNormalsPipeline = try Terrain.computePipeline(withFunctionNamed: "updateGeometryVertexNormals", device: device, library: library) + } catch { + print("Unable to create compute pipelines for terrain geometry updates. Error: \(error)") + return nil + } + super.init() } + + func generate(completion: @escaping () -> Void) -> Progress { + let progress = Progress(totalUnitCount: 3) + generatorQueue.async { + progress.becomeCurrent(withPendingUnitCount: 3) + + let heights = self.generator.render(progress: progress) + progress.completedUnitCount += 1 + + // TODO: Store heights + progress.completedUnitCount += 1 + + // TODO: Compute normals + progress.completedUnitCount += 1 + + progress.resignCurrent() + completion() + } + return progress + } }