Implement progress tracking
This commit is contained in:
parent
58f59baaf5
commit
2b2539d7df
5 changed files with 116 additions and 26 deletions
|
@ -25,7 +25,7 @@ protocol TerrainGenerator {
|
||||||
|
|
||||||
func updateUniforms()
|
func updateUniforms()
|
||||||
func encode(in encoder: MTLComputeCommandEncoder)
|
func encode(in encoder: MTLComputeCommandEncoder)
|
||||||
func render(completion: @escaping () -> Void)
|
func render(progress: Progress) -> [Float]
|
||||||
}
|
}
|
||||||
|
|
||||||
class Kernel {
|
class Kernel {
|
||||||
|
@ -99,7 +99,9 @@ class ZeroAlgorithm: Kernel, TerrainGenerator {
|
||||||
|
|
||||||
func updateUniforms() { }
|
func updateUniforms() { }
|
||||||
|
|
||||||
func render(completion: @escaping () -> Void) { }
|
func render(progress: Progress) -> [Float] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Randomly generate heights that are independent of all others.
|
/// Randomly generate heights that are independent of all others.
|
||||||
|
@ -133,7 +135,9 @@ class RandomAlgorithm: Kernel, TerrainGenerator {
|
||||||
RandomAlgorithmUniforms_refreshRandoms(uniforms)
|
RandomAlgorithmUniforms_refreshRandoms(uniforms)
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(completion: @escaping () -> Void) { }
|
func render(progress: Progress) -> [Float] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation of the Diamond-Squares algorithm.
|
/// 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<Box>()
|
var queue = Queue<Box>()
|
||||||
|
|
||||||
queue.enqueue(item: self)
|
queue.enqueue(item: self)
|
||||||
|
progress.totalUnitCount += 1
|
||||||
|
|
||||||
while let box = queue.dequeue() {
|
while let box = queue.dequeue() {
|
||||||
visit(box)
|
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 {
|
struct Algorithm {
|
||||||
let grid: Box
|
let grid: Box
|
||||||
|
|
||||||
private let queue = DispatchQueue(label: "Diamond-Square Algorithm Queue")
|
|
||||||
|
|
||||||
var roughness: Float = 1.0 {
|
var roughness: Float = 1.0 {
|
||||||
didSet {
|
didSet {
|
||||||
randomRange = -roughness...roughness
|
randomRange = -roughness...roughness
|
||||||
|
@ -253,14 +262,9 @@ public class DiamondSquareGenerator: TerrainGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the algorithm and return the genreated height map.
|
/// Run the algorithm and return the genreated height map.
|
||||||
func render(completion: @escaping ([Float]) -> Void) {
|
func render(progress: Progress) -> [Float] {
|
||||||
queue.async {
|
let renderProgress = Progress(totalUnitCount: 1, parent: progress, pendingUnitCount: 1)
|
||||||
let heightMap = self.queue_render()
|
|
||||||
completion(heightMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func queue_render() -> [Float] {
|
|
||||||
os_signpost(.begin, log: Log, name: "DiamondSquare.render")
|
os_signpost(.begin, log: Log, name: "DiamondSquare.render")
|
||||||
|
|
||||||
var heightMap = [Float](repeating: 0, count: grid.size.w * grid.size.h)
|
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)
|
let idx = convert(pointToIndex: p)
|
||||||
heightMap[idx] = Float.random(in: randomRange)
|
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.
|
// 1. Diamond step. Find the midpoint of the square defined by `box` and set its value.
|
||||||
let midpoint = box.midpoint
|
let midpoint = box.midpoint
|
||||||
let cornerValues = box.corners.map { (pt: Point) -> Float in
|
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)))
|
algorithm = Algorithm(grid: Box(origin: Point(), size: Size(w: DiamondSquareGenerator.textureSize.width, h: DiamondSquareGenerator.textureSize.height)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(completion: @escaping () -> Void) {
|
func render(progress: Progress) -> [Float] {
|
||||||
algorithm.render { (heightMap: [Float]) in
|
let algProgress = Progress(totalUnitCount: 2, parent: progress, pendingUnitCount: 2)
|
||||||
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<Float>.stride * DiamondSquareGenerator.textureSize.width)
|
|
||||||
self.activeTexture = newActiveTexture
|
|
||||||
|
|
||||||
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<Float>.stride * DiamondSquareGenerator.textureSize.width)
|
||||||
|
self.activeTexture = newActiveTexture
|
||||||
|
algProgress.completedUnitCount += 1
|
||||||
|
|
||||||
|
return heights
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Algorithm
|
// MARK: Algorithm
|
||||||
|
|
|
@ -14,6 +14,9 @@ class GameViewController: NSViewController {
|
||||||
|
|
||||||
var renderer: Renderer!
|
var renderer: Renderer!
|
||||||
var mtkView: MTKView!
|
var mtkView: MTKView!
|
||||||
|
var progressIndicator: NSProgressIndicator!
|
||||||
|
|
||||||
|
private var progressObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
@ -41,6 +44,17 @@ class GameViewController: NSViewController {
|
||||||
renderer.mtkView(mtkView, drawableSizeWillChange: mtkView.drawableSize)
|
renderer.mtkView(mtkView, drawableSizeWillChange: mtkView.drawableSize)
|
||||||
|
|
||||||
mtkView.delegate = renderer
|
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() {
|
override func viewWillAppear() {
|
||||||
|
@ -55,7 +69,23 @@ class GameViewController: NSViewController {
|
||||||
override func keyDown(with event: NSEvent) {
|
override func keyDown(with event: NSEvent) {
|
||||||
switch event.charactersIgnoringModifiers {
|
switch event.charactersIgnoringModifiers {
|
||||||
case .some("n"):
|
case .some("n"):
|
||||||
renderer.scheduleAlgorithmIteration()
|
if let progress = renderer.scheduleAlgorithmIteration() {
|
||||||
|
progressIndicator.isHidden = false
|
||||||
|
progressObservation = progress.observe(\.fractionCompleted) { [weak self] (progress: Progress, change: NSKeyValueObservedChange<Double>) 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"):
|
case .some("z"):
|
||||||
renderer.drawLines = !renderer.drawLines
|
renderer.drawLines = !renderer.drawLines
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -166,16 +166,18 @@ class Renderer: NSObject, MTKViewDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func scheduleAlgorithmIteration() {
|
func scheduleAlgorithmIteration() -> Progress? {
|
||||||
|
var progress: Progress? = nil
|
||||||
regenerationSemaphore.wait()
|
regenerationSemaphore.wait()
|
||||||
if !terrain.generator.needsGPU {
|
if !terrain.generator.needsGPU {
|
||||||
print("Rendering terrain...")
|
print("Rendering terrain...")
|
||||||
self.terrain.generator.render {
|
progress = self.terrain.generate {
|
||||||
print("Rendering terrain...complete!")
|
print("Rendering terrain...complete!")
|
||||||
self.didUpdateTerrain = true
|
self.didUpdateTerrain = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
regenerationSemaphore.signal()
|
regenerationSemaphore.signal()
|
||||||
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateDynamicBufferState() {
|
private func updateDynamicBufferState() {
|
||||||
|
|
|
@ -73,6 +73,11 @@ kernel void updateGeometryNormals(constant float3 *vertexes [[buffer(GeneratorBu
|
||||||
normals[tid] = normal;
|
normals[tid] = normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kernel void updateGeometryVertexNormals()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - ZeroGenerator
|
#pragma mark - ZeroGenerator
|
||||||
|
|
||||||
kernel void zeroKernel(texture2d<float, access::write> outTexture [[texture(GeneratorTextureIndexOut)]],
|
kernel void zeroKernel(texture2d<float, access::write> outTexture [[texture(GeneratorTextureIndexOut)]],
|
||||||
|
|
|
@ -69,6 +69,20 @@ class Terrain: NSObject {
|
||||||
|
|
||||||
return try MTKMesh(mesh:plane, device:device)
|
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 dimensions: float2
|
||||||
let segments: uint2
|
let segments: uint2
|
||||||
|
@ -96,6 +110,35 @@ class Terrain: NSObject {
|
||||||
(gen as DiamondSquareGenerator).roughness = 0.075
|
(gen as DiamondSquareGenerator).roughness = 0.075
|
||||||
generator = gen
|
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()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue