Skip to content

Commit

Permalink
Added multithreaded rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklockwood committed Apr 25, 2020
1 parent 3a5771d commit 8e8a7ba
Showing 6 changed files with 118 additions and 62 deletions.
4 changes: 4 additions & 0 deletions Source/Rampage.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -64,6 +64,7 @@
01D0F5D922F80E1600682CA1 /* RampageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D0F5D822F80E1600682CA1 /* RampageTests.swift */; };
01D0F5F122FF095E00682CA1 /* Door.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D0F5F022FF095E00682CA1 /* Door.swift */; };
01E3963A2342758D00D02236 /* Pushwall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E396392342758D00D02236 /* Pushwall.swift */; };
01D0F5EF22FDF19A00682CA1 /* Bitmap+Rendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D0F5EE22FDF19900682CA1 /* Bitmap+Rendering.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
@@ -174,6 +175,7 @@
01D0F5DA22F80E1600682CA1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
01D0F5F022FF095E00682CA1 /* Door.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Door.swift; sourceTree = "<group>"; };
01E396392342758D00D02236 /* Pushwall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pushwall.swift; sourceTree = "<group>"; };
01D0F5EE22FDF19900682CA1 /* Bitmap+Rendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bitmap+Rendering.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
@@ -256,6 +258,7 @@
children = (
016E41B2228E9A5B00ACF137 /* AppDelegate.swift */,
016E41B4228E9A5B00ACF137 /* ViewController.swift */,
01D0F5EE22FDF19900682CA1 /* Bitmap+Rendering.swift */,
01D09AF422A482450052745A /* UIImage+Bitmap.swift */,
0199F57323E242D4003E3F08 /* SoundManager.swift */,
016E41B6228E9A5B00ACF137 /* Main.storyboard */,
@@ -544,6 +547,7 @@
files = (
01D09AF522A482450052745A /* UIImage+Bitmap.swift in Sources */,
0199F57423E242D4003E3F08 /* SoundManager.swift in Sources */,
01D0F5EF22FDF19A00682CA1 /* Bitmap+Rendering.swift in Sources */,
016E41B5228E9A5B00ACF137 /* ViewController.swift in Sources */,
016E41B3228E9A5B00ACF137 /* AppDelegate.swift in Sources */,
);
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
34 changes: 34 additions & 0 deletions Source/Rampage/Bitmap+Rendering.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// ConcurrentRenderer.swift
// Rampage
//
// Created by Nick Lockwood on 09/08/2019.
// Copyright © 2019 Nick Lockwood. All rights reserved.
//

import UIKit
import Engine
import Renderer

public extension Bitmap {
init(width: Int, height: Int, safeArea: Rect, world: World, textures: Textures) {
let cpuCores = ProcessInfo.processInfo.activeProcessorCount
var buffers = Array(repeating: [Color](), count: cpuCores)
let step = 1.0 / Double(cpuCores)
DispatchQueue.concurrentPerform(iterations: cpuCores) { i in
let offset = Double(i) * step
let range = offset ..< offset + step
var renderer = Renderer(
width: width,
height: height,
range: range,
textures: textures
)
renderer.safeArea = safeArea
renderer.draw(world)
buffers[i] = renderer.bitmap.pixels
}
let pixels = Array(buffers.joined())
self.init(height: height, pixels: pixels)
}
}
20 changes: 13 additions & 7 deletions Source/Rampage/ViewController.swift
Original file line number Diff line number Diff line change
@@ -132,15 +132,21 @@ class ViewController: UIViewController {
lastFrameTime = displayLink.timestamp

let width = Int(imageView.bounds.width), height = Int(imageView.bounds.height)
var renderer = Renderer(width: width, height: height, textures: textures)
let insets = self.view.safeAreaInsets
renderer.safeArea = Rect(
min: Vector(x: Double(insets.left), y: Double(insets.top)),
max: renderer.bitmap.size - Vector(x: Double(insets.left), y: Double(insets.bottom))
let bitmap = Bitmap(
width: width,
height: height,
safeArea: Rect(
min: Vector(x: Double(insets.left), y: Double(insets.top)),
max: Vector(
x: Double(width) - Double(insets.left),
y: Double(height) - Double(insets.bottom)
)
),
world: world,
textures: textures
)
renderer.draw(world)

imageView.image = UIImage(bitmap: renderer.bitmap)
imageView.image = UIImage(bitmap: bitmap)
}

@objc func fire(_ gestureRecognizer: UITapGestureRecognizer) {
3 changes: 1 addition & 2 deletions Source/RampageTests/RampageTests.swift
Original file line number Diff line number Diff line change
@@ -17,8 +17,7 @@ class RampageTests: XCTestCase {

func testRenderFrame() {
self.measure {
var renderer = Renderer(width: 1000, height: 1000, textures: textures)
renderer.draw(world)
_ = Bitmap(width: 1000, height: 1000, world: world, textures: textures)
}
}
}
117 changes: 65 additions & 52 deletions Source/Renderer/Renderer.swift
Original file line number Diff line number Diff line change
@@ -12,11 +12,16 @@ private let fizzle = (0 ..< 10000).shuffled()

public struct Renderer {
public private(set) var bitmap: Bitmap
private let width: Int
private let range: Range<Double>
private let textures: Textures
public var safeArea: Rect

public init(width: Int, height: Int, textures: Textures) {
self.bitmap = Bitmap(width: width, height: height, color: .black)
public init(width: Int, height: Int, range: Range<Double>, textures: Textures) {
self.width = width
self.range = range
let columns = Int(Double(width) * (range.upperBound - range.lowerBound))
self.bitmap = Bitmap(width: columns, height: height, color: .clear)
self.textures = textures
self.safeArea = Rect(min: Vector(x: 0, y: 0), max: bitmap.size)
}
@@ -25,16 +30,15 @@ public struct Renderer {
public extension Renderer {
mutating func draw(_ world: World) {
let focalLength = 1.0
let viewWidth = Double(bitmap.width) / Double(bitmap.height)
let viewWidth = Double(width) / Double(bitmap.height)
let viewPlane = world.player.direction.orthogonal * viewWidth
let viewCenter = world.player.position + world.player.direction * focalLength
let viewStart = viewCenter - viewPlane / 2
let viewStart = viewCenter - viewPlane * (0.5 - range.lowerBound)

// Cast rays
let columns = bitmap.width
let step = viewPlane / Double(columns)
let step = viewPlane / Double(width)
var columnPosition = viewStart
for x in 0 ..< columns {
for x in 0 ..< bitmap.width {
let rayDirection = columnPosition - world.player.position
let viewPlaneDistance = rayDirection.length
let ray = Ray(
@@ -132,64 +136,73 @@ public extension Renderer {
let weaponWidth = screenHeight * aspectRatio
bitmap.drawImage(
weaponTexture,
at: Vector(x: Double(bitmap.width) / 2 - weaponWidth / 2, y: 0),
at: Vector(x: Double(width) * (0.5 - range.lowerBound) - weaponWidth / 2, y: 0),
size: Vector(x: weaponWidth, y: screenHeight)
)

// Crosshair
let crosshair = textures[.crosshair]
let hudScale = bitmap.size.y / 64
let crosshairSize = crosshair.size * hudScale
bitmap.drawImage(crosshair, at: (bitmap.size - crosshairSize) / 2, size: crosshairSize)

// Health icon
let healthIcon = textures[.healthIcon]
var offset = safeArea.min + Vector(x: 1, y: 1) * hudScale
bitmap.drawImage(healthIcon, at: offset, size: healthIcon.size * hudScale)
offset.x += healthIcon.size.x * hudScale
bitmap.drawImage(
crosshair,
at: Vector(x: Double(width) * (0.5 - range.lowerBound) - crosshairSize.x / 2,
y: (bitmap.size.y - crosshairSize.y) / 2),
size: crosshairSize)

// Health
let font = textures[.font]
let charSize = Vector(x: font.size.x / 10, y: font.size.y)
let health = Int(max(0, world.player.health))
let healthTint: Color
switch health {
case ...10:
healthTint = .red
case 10 ... 30:
healthTint = .yellow
default:
healthTint = .green
}
for char in String(health) {
let index = Int(char.asciiValue!) - 48
let step = Int(charSize.x)
let xRange = index * step ..< (index + 1) * step
bitmap.drawImage(
font,
xRange: xRange,
at: offset,
size: charSize * hudScale,
tint: healthTint
)
offset.x += charSize.x * hudScale
}
var offset = safeArea.min + Vector(x: 1, y: 1) * hudScale

// Ammunition
offset.x = safeArea.max.x
let ammo = Int(max(0, min(99, world.player.ammo)))
for char in String(ammo).reversed() {
let index = Int(char.asciiValue!) - 48
let step = Int(charSize.x)
let xRange = index * step ..< (index + 1) * step
offset.x -= charSize.x * hudScale
bitmap.drawImage(font, xRange: xRange, at: offset, size: charSize * hudScale)
if range.lowerBound == 0 {
// Health icon
let healthIcon = textures[.healthIcon]
bitmap.drawImage(healthIcon, at: offset, size: healthIcon.size * hudScale)
offset.x += healthIcon.size.x * hudScale

// Health
let health = Int(max(0, world.player.health))
let healthTint: Color
switch health {
case ...10:
healthTint = .red
case 10 ... 30:
healthTint = .yellow
default:
healthTint = .green
}
for char in String(health) {
let index = Int(char.asciiValue!) - 48
let step = Int(charSize.x)
let xRange = index * step ..< (index + 1) * step
bitmap.drawImage(
font,
xRange: xRange,
at: offset,
size: charSize * hudScale,
tint: healthTint
)
offset.x += charSize.x * hudScale
}
}

// Weapon icon
let weaponIcon = textures[world.player.weapon.attributes.hudIcon]
offset.x -= weaponIcon.size.x * hudScale
bitmap.drawImage(weaponIcon, at: offset, size: weaponIcon.size * hudScale)
if range.upperBound == 1 {
// Ammunition
offset.x = safeArea.max.x - range.lowerBound * Double(width)
let ammo = Int(max(0, min(99, world.player.ammo)))
for char in String(ammo).reversed() {
let index = Int(char.asciiValue!) - 48
let step = Int(charSize.x)
let xRange = index * step ..< (index + 1) * step
offset.x -= charSize.x * hudScale
bitmap.drawImage(font, xRange: xRange, at: offset, size: charSize * hudScale)
}

// Weapon icon
let weaponIcon = textures[world.player.weapon.attributes.hudIcon]
offset.x -= weaponIcon.size.x * hudScale
bitmap.drawImage(weaponIcon, at: offset, size: weaponIcon.size * hudScale)
}

// Effects
for effect in world.effects {

0 comments on commit 8e8a7ba

Please sign in to comment.