Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions SnowfallApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"SnowfallApp/Preview Content\"";
DEVELOPMENT_TEAM = 694AARYR2X;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
Expand Down Expand Up @@ -317,6 +318,7 @@
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"SnowfallApp/Preview Content\"";
DEVELOPMENT_TEAM = 694AARYR2X;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2610"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97FD20C72CFDBC2C00A64237"
BuildableName = "SnowfallApp.app"
BlueprintName = "SnowfallApp"
ReferencedContainer = "container:SnowfallApp.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97FD20C72CFDBC2C00A64237"
BuildableName = "SnowfallApp.app"
BlueprintName = "SnowfallApp"
ReferencedContainer = "container:SnowfallApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97FD20C72CFDBC2C00A64237"
BuildableName = "SnowfallApp.app"
BlueprintName = "SnowfallApp"
ReferencedContainer = "container:SnowfallApp.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ enum SnowPreset: String, CaseIterable, Codable {
case custom = "Свой"
}

final class Settings: Codable {
static let shared = Settings()
class Settings: Codable {
static var shared = Settings()

var currentPreset: SnowPreset = .comfort
var isPaused: Bool = false
Expand All @@ -26,46 +26,28 @@ final class Settings: Codable {
var meltingSpeed: Float = 0.05
var windowInteraction: Bool = true

private init() {
load()
}
private init() {}

func applyPreset(_ preset: SnowPreset) {
currentPreset = preset
preset.apply(to: self)
save()
static func reset() {
UserDefaults.standard.dictionaryRepresentation().keys.forEach({ UserDefaults.standard.removeObject(forKey: $0) })
load()
}

func reset() {
UserDefaults.standard.dictionaryRepresentation().keys.forEach {
UserDefaults.standard.removeObject(forKey: $0)
static func save() {
if let data = try? JSONEncoder().encode(shared) {
UserDefaults().set(data, forKey: "settings")
}
load()
}

func save() {
guard let data = try? JSONEncoder().encode(self) else { return }

UserDefaults.standard.set(data, forKey: "settings")
static func load() {
let settings = UserDefaults().data(forKey: "settings").flatMap({ try? JSONDecoder().decode(Settings.self, from: $0) })
shared = settings ?? Settings()
}

private func load() {
guard let data = UserDefaults.standard.data(forKey: "settings"),
let settings = try? JSONDecoder().decode(Settings.self, from: data) else {
applyPreset(.comfort)
return
}

currentPreset = settings.currentPreset
isPaused = settings.isPaused
displayMode = settings.displayMode
pauseInFullscreen = settings.pauseInFullscreen
snowflakeSizeRange = settings.snowflakeSizeRange
maxSnowflakes = settings.maxSnowflakes
snowflakeSpeedRange = settings.snowflakeSpeedRange
windStrength = settings.windStrength
meltingSpeed = settings.meltingSpeed
windowInteraction = settings.windowInteraction
func applyPreset(_ preset: SnowPreset) {
self.currentPreset = preset
preset.apply(to: self)
Settings.save()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,46 @@ struct SnowUniforms {
var windStrength: Float
var minSize: Float
var maxSize: Float
var minSpeed: Float
var maxSpeed: Float
var isWindowInteractionEnabled: Bool
var particleCount: Float
var _padding: (Bool, Bool, Bool) = (false, false, false)
}

final class SnowRenderer: NSObject {
class SnowRenderer: NSObject, MTKViewDelegate {
private var device: MTLDevice!
private var commandQueue: MTLCommandQueue!
private var particleBuffer: MTLBuffer!

private var renderPipelineState: MTLRenderPipelineState!
private var computePipelineState: MTLComputePipelineState!
private var initComputePipelineState: MTLComputePipelineState!
private var particleCount: Int = 0
private var globalRect: CGRect!
private var screenRect: CGRect!

private var snowflakes: [Snowflake] = []

var mousePosition: simd_float2 = simd_float2(-1000, -1000)
var screenSize: simd_float2 = .zero

private var cachedWindowRect: CGRect = .zero
private var lastWindowCheckTime: TimeInterval = 0
private let windowCheckInterval: TimeInterval = 0.5

private var lastDrawTime: CFTimeInterval = CACurrentMediaTime()

init(mtkView: MTKView, screenRect: CGRect, globalRect: CGRect) {
init(mtkView: MTKView, screenSize: CGSize) {
super.init()
self.device = mtkView.device
self.globalRect = globalRect
self.screenRect = screenRect
self.commandQueue = device.makeCommandQueue()
mtkView.colorPixelFormat = .bgra8Unorm
mtkView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
mtkView.layer?.isOpaque = false

createPipelineStates()

self.screenSize = SIMD2(Float(screenRect.size.width), Float(screenRect.size.height))
self.screenSize = SIMD2(Float(screenSize.width), Float(screenSize.height))
generateSnowflakes()
}

private func createPipelineStates() {
let library = device.makeDefaultLibrary()

// Render pipeline
let vertexFunction = library?.makeFunction(name: "vertex_main")
let fragmentFunction = library?.makeFunction(name: "fragment_main")

Expand All @@ -78,78 +73,20 @@ final class SnowRenderer: NSObject {
print("Render Pipeline Error: \(error)")
}

// Update compute pipeline
if let computeFunction = library?.makeFunction(name: "updateSnowflakes") {
do {
computePipelineState = try device.makeComputePipelineState(function: computeFunction)
} catch {
print("Compute Pipeline Error: \(error)")
}
}

// Init compute pipeline
if let initFunction = library?.makeFunction(name: "initializeSnowflakes") {
do {
initComputePipelineState = try device.makeComputePipelineState(function: initFunction)
} catch {
print("Init Compute Pipeline Error: \(error)")
}
}
}

private func generateSnowflakes() {
func generateSnowflakes() {
let maxSnowflakes = Settings.shared.maxSnowflakes
particleCount = maxSnowflakes

let bufferSize = maxSnowflakes * MemoryLayout<Snowflake>.stride
particleBuffer = device.makeBuffer(length: bufferSize, options: .storageModeShared)

initializeParticlesOnGPU()
}

private func initializeParticlesOnGPU() {
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let initKernel = initComputePipelineState else { return }

var uniforms = makeCurrentUniforms()

let computeEncoder = commandBuffer.makeComputeCommandEncoder()!
computeEncoder.setComputePipelineState(initKernel)
computeEncoder.setBuffer(particleBuffer, offset: 0, index: 0)
computeEncoder.setBytes(&uniforms, length: MemoryLayout<SnowUniforms>.stride, index: 1)

let width = initKernel.threadExecutionWidth
let threadsPerGrid = MTLSize(width: particleCount, height: 1, depth: 1)
let threadsPerGroup = MTLSize(width: width, height: 1, depth: 1)
computeEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup)
computeEncoder.endEncoding()

commandBuffer.commit()
commandBuffer.waitUntilCompleted()
}

private func makeCurrentUniforms() -> SnowUniforms {
let cachedWindowRectLocalOrigin = WindowInfo().cast(from: globalRect, to: screenRect, point: cachedWindowRect.origin)
return SnowUniforms(
screenSize: screenSize,
mousePosition: mousePosition,
windowRect: simd_float4(
// cast window origin from global to local
Float(cachedWindowRectLocalOrigin.x),
Float(cachedWindowRectLocalOrigin.y),
Float(cachedWindowRect.width),
Float(cachedWindowRect.height)
),
time: Float(CACurrentMediaTime()),
deltaTime: Float(CACurrentMediaTime() - lastDrawTime),
windStrength: Settings.shared.windStrength,
minSize: Settings.shared.snowflakeSizeRange.lowerBound,
maxSize: Settings.shared.snowflakeSizeRange.upperBound,
minSpeed: Settings.shared.snowflakeSpeedRange.lowerBound,
maxSpeed: Settings.shared.snowflakeSpeedRange.upperBound,
isWindowInteractionEnabled: Settings.shared.windowInteraction,
particleCount: Float(particleCount)
)
snowflakes = (0..<maxSnowflakes).map { _ in Snowflake(for: screenSize) }
let bufferSize = snowflakes.count * MemoryLayout<Snowflake>.stride
particleBuffer = device.makeBuffer(bytes: snowflakes, length: bufferSize, options: .storageModeShared)
}

private func updateActiveWindowRect() {
Expand All @@ -163,31 +100,31 @@ final class SnowRenderer: NSObject {
lastWindowCheckTime = currentTime
}
}
}

extension SnowRenderer: MTKViewDelegate {
internal func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
self.screenSize = SIMD2(Float(size.width), Float(size.height))
}

internal func draw(in view: MTKView) {
func draw(in view: MTKView) {
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let drawable = view.currentDrawable,
let renderPassDescriptor = view.currentRenderPassDescriptor,
let computePipeline = computePipelineState else { return }

if particleCount != Settings.shared.maxSnowflakes {
generateSnowflakes()
}

if snowflakes.count != Settings.shared.maxSnowflakes { generateSnowflakes() }
updateActiveWindowRect()

let currentTime = CACurrentMediaTime()
let deltaTime = Float(currentTime - lastDrawTime)
lastDrawTime = currentTime

var uniforms = makeCurrentUniforms()
uniforms.deltaTime = deltaTime
var uniforms = SnowUniforms(
screenSize: screenSize,
mousePosition: mousePosition,
windowRect: simd_float4(Float(cachedWindowRect.origin.x), Float(cachedWindowRect.origin.y), Float(cachedWindowRect.width), Float(cachedWindowRect.height)),
time: Float(currentTime),
deltaTime: deltaTime,
windStrength: Settings.shared.windStrength,
minSize: Settings.shared.snowflakeSizeRange.lowerBound,
maxSize: Settings.shared.snowflakeSizeRange.upperBound,
isWindowInteractionEnabled: Settings.shared.windowInteraction
)

// Compute
let computeEncoder = commandBuffer.makeComputeCommandEncoder()!
Expand All @@ -196,7 +133,7 @@ extension SnowRenderer: MTKViewDelegate {
computeEncoder.setBytes(&uniforms, length: MemoryLayout<SnowUniforms>.stride, index: 1)

let width = computePipeline.threadExecutionWidth
let threadsPerGrid = MTLSize(width: particleCount, height: 1, depth: 1)
let threadsPerGrid = MTLSize(width: snowflakes.count, height: 1, depth: 1)
let threadsPerGroup = MTLSize(width: width, height: 1, depth: 1)
computeEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup)
computeEncoder.endEncoding()
Expand All @@ -206,10 +143,14 @@ extension SnowRenderer: MTKViewDelegate {
renderEncoder.setRenderPipelineState(renderPipelineState)
renderEncoder.setVertexBuffer(particleBuffer, offset: 0, index: 0)
renderEncoder.setVertexBytes(&uniforms, length: MemoryLayout<SnowUniforms>.stride, index: 1)
renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: particleCount)
renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: snowflakes.count)
renderEncoder.endEncoding()

commandBuffer.present(drawable)
commandBuffer.commit()
}

func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
self.screenSize = SIMD2(Float(size.width), Float(size.height))
}
}
Loading