Initial Commit
This commit is contained in:
commit
b50da2cb27
6 changed files with 1298 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/configuration/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
||||||
43
Package.swift
Normal file
43
Package.swift
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// swift-tools-version: 6.1
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "ArtifactGraphics",
|
||||||
|
platforms: [.macOS(.v14)],
|
||||||
|
products: [
|
||||||
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
|
.library(
|
||||||
|
name: "ArtifactGraphics",
|
||||||
|
targets: ["ArtifactGraphics"]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
traits: [
|
||||||
|
.default(enabledTraits: ["WebGPU"]),
|
||||||
|
.trait(name: "WebGPU", description: "Enables the builtin WebGPU backend.")
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(path: "../ArtifactPlatform"),
|
||||||
|
.package(path: "../ArtifactColor"),
|
||||||
|
.package(path: "../ArtifactMath"),
|
||||||
|
.package(path: "../WGPUNative"),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||||
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
|
.target(
|
||||||
|
name: "ArtifactGraphics",
|
||||||
|
dependencies: [
|
||||||
|
"ArtifactColor",
|
||||||
|
"ArtifactPlatform",
|
||||||
|
"ArtifactMath",
|
||||||
|
.product(name: "WGPUNative", package: "WGPUNative", condition: .when(traits: ["WebGPU"])),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "ArtifactGraphicsTests",
|
||||||
|
dependencies: ["ArtifactGraphics"]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
119
Sources/ArtifactGraphics/ArtifactGraphics.swift
Normal file
119
Sources/ArtifactGraphics/ArtifactGraphics.swift
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
|
||||||
|
import ArtifactMath
|
||||||
|
import ArtifactPlatform
|
||||||
|
|
||||||
|
public protocol Graphics {
|
||||||
|
@MainActor
|
||||||
|
init(_ window: any Window)
|
||||||
|
|
||||||
|
var scene: [Drawable] { get set }
|
||||||
|
|
||||||
|
func createBuffer(label: String, usage: [BufferUsage], size: Int) -> any Buffer
|
||||||
|
func createTexture(label: String, usage: [TextureUsage], size: [UInt32]) -> any Texture
|
||||||
|
func createSampler(label: String) -> any Sampler
|
||||||
|
|
||||||
|
func resizeViewport(width: Int, height: Int)
|
||||||
|
func draw(alpha: Double)
|
||||||
|
func reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Drawable {
|
||||||
|
public enum Operation {
|
||||||
|
case draw(count: Int)
|
||||||
|
case pushScissor(rect: Rect<Float>)
|
||||||
|
case popScissor
|
||||||
|
case pushTransform(_ transformation: Mat4f)
|
||||||
|
case popTransform
|
||||||
|
}
|
||||||
|
|
||||||
|
var passes: Set<String>?
|
||||||
|
public var vertices: any Buffer
|
||||||
|
public var vertexCount: Int
|
||||||
|
public var indices: (any Buffer)?
|
||||||
|
public var indexCount: Int?
|
||||||
|
public var uniforms: [UInt8]?
|
||||||
|
public var operations: [Operation]?
|
||||||
|
|
||||||
|
public init(passes: Set<String>? = nil, vertices: any Buffer, vertexCount: Int, indices: (any Buffer)? = nil, indexCount: Int? = nil, uniforms: [UInt8]? = nil, operations: [Operation]? = nil) {
|
||||||
|
self.passes = passes
|
||||||
|
self.vertices = vertices
|
||||||
|
self.vertexCount = vertexCount
|
||||||
|
self.indices = indices
|
||||||
|
self.indexCount = indexCount
|
||||||
|
self.uniforms = uniforms
|
||||||
|
self.operations = operations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol Buffer {
|
||||||
|
var size: UInt64 { get }
|
||||||
|
|
||||||
|
func writeData(_ data: [UInt8])
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BufferUsage: UInt32, Sendable, CaseIterable {
|
||||||
|
case vertex
|
||||||
|
case index
|
||||||
|
case uniform
|
||||||
|
case storage
|
||||||
|
case indirect
|
||||||
|
|
||||||
|
case copySrc
|
||||||
|
case copyDest
|
||||||
|
case mapRead
|
||||||
|
case mapWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol Texture {
|
||||||
|
var width: UInt32 { get }
|
||||||
|
var height: UInt32 { get }
|
||||||
|
var depth: UInt32 { get }
|
||||||
|
var dimensions: Int { get }
|
||||||
|
|
||||||
|
func writeData(_ data: [UInt8])
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TextureUsage {
|
||||||
|
case storage
|
||||||
|
case texture
|
||||||
|
case attachment
|
||||||
|
|
||||||
|
case copySrc
|
||||||
|
case copyDest
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol Sampler {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AddressMode {
|
||||||
|
case `repeat`
|
||||||
|
case mirrorRepeat
|
||||||
|
case clamp
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ShaderStage {
|
||||||
|
case vertex
|
||||||
|
case fragment
|
||||||
|
case compute
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BufferBindingType {
|
||||||
|
case storage
|
||||||
|
case readOnlyStorage
|
||||||
|
case uniform
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SamplerBindingType {
|
||||||
|
case filtering
|
||||||
|
case comparison
|
||||||
|
case nonfiltering
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum TextureSampleType {
|
||||||
|
case sint
|
||||||
|
case uint
|
||||||
|
case float
|
||||||
|
case depth
|
||||||
|
}
|
||||||
|
|
||||||
217
Sources/ArtifactGraphics/RenderPass.swift
Normal file
217
Sources/ArtifactGraphics/RenderPass.swift
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
|
||||||
|
import ArtifactColor
|
||||||
|
|
||||||
|
public enum RenderPassType {
|
||||||
|
case graphics
|
||||||
|
case compute
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RenderPassDataType {
|
||||||
|
case i32
|
||||||
|
case f32
|
||||||
|
case vec3f
|
||||||
|
case vec3i
|
||||||
|
case vec2f
|
||||||
|
case vec21
|
||||||
|
case mat4f
|
||||||
|
case mat4i
|
||||||
|
case sampler
|
||||||
|
case buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RenderPassBinding {
|
||||||
|
case buffer(stage: ShaderStage, type: BufferBindingType, buffer: any Buffer)
|
||||||
|
case sampler(stage: ShaderStage, type: SamplerBindingType, sampler: any Sampler)
|
||||||
|
case texture(stage: ShaderStage, sampleType: TextureSampleType, texture: any Texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Shader {
|
||||||
|
let entryPoint: StaticString
|
||||||
|
var source: String
|
||||||
|
var constants: [String: Double]
|
||||||
|
|
||||||
|
public init(entryPoint: StaticString = "main", source: String, constants: [String: Double] = [:]) {
|
||||||
|
self.entryPoint = entryPoint
|
||||||
|
self.source = source
|
||||||
|
self.constants = constants
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PrimitiveTopology {
|
||||||
|
case points
|
||||||
|
case lines
|
||||||
|
case lineStrip
|
||||||
|
case triangles
|
||||||
|
case triangleStrip
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum WindingOrder {
|
||||||
|
case clockwise
|
||||||
|
case counterclockwise
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VertexStepMode {
|
||||||
|
case vertex
|
||||||
|
case instance
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VertexFormat {
|
||||||
|
case uint8
|
||||||
|
case uint16
|
||||||
|
case uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VertexAttributeType: UInt32 {
|
||||||
|
case uint8
|
||||||
|
|
||||||
|
case vec2f
|
||||||
|
case vec3f
|
||||||
|
case vec4f
|
||||||
|
|
||||||
|
case vec2d
|
||||||
|
case vec3d
|
||||||
|
case vec4d
|
||||||
|
|
||||||
|
case vec2u
|
||||||
|
case vec3u
|
||||||
|
case vec4u
|
||||||
|
|
||||||
|
case vec2n
|
||||||
|
case vec3n
|
||||||
|
case vec4n
|
||||||
|
|
||||||
|
var size: Int {
|
||||||
|
switch self {
|
||||||
|
case .uint8: MemoryLayout<UInt8>.size
|
||||||
|
|
||||||
|
case .vec2f: MemoryLayout<Float>.size * 2
|
||||||
|
case .vec3f: MemoryLayout<Float>.size * 3
|
||||||
|
case .vec4f: MemoryLayout<Float>.size * 4
|
||||||
|
|
||||||
|
case .vec2d: MemoryLayout<Double>.size * 2
|
||||||
|
case .vec3d: MemoryLayout<Double>.size * 3
|
||||||
|
case .vec4d: MemoryLayout<Double>.size * 4
|
||||||
|
|
||||||
|
case .vec2u, .vec2n: MemoryLayout<UInt8>.size * 2
|
||||||
|
case .vec3u, .vec3n: MemoryLayout<UInt8>.size * 3
|
||||||
|
case .vec4u, .vec4n: MemoryLayout<UInt8>.size * 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct VertexBufferLayout {
|
||||||
|
let stepMode: VertexStepMode
|
||||||
|
let attributes: [VertexAttributeType]
|
||||||
|
|
||||||
|
public init(stepMode: VertexStepMode = .vertex, attributes: [VertexAttributeType]) {
|
||||||
|
self.stepMode = stepMode
|
||||||
|
self.attributes = attributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BlendFactor: UInt32, Sendable, CaseIterable {
|
||||||
|
case zero
|
||||||
|
case one
|
||||||
|
case src
|
||||||
|
case oneMinusSrc
|
||||||
|
case srcAlpha
|
||||||
|
case oneMinusSrcAlpha
|
||||||
|
case dest
|
||||||
|
case oneMinusDest
|
||||||
|
case dstAlpha
|
||||||
|
case oneMinusDstAlpha
|
||||||
|
case srcAlphaSaturated
|
||||||
|
case constant
|
||||||
|
case oneMinusConstant
|
||||||
|
case src1
|
||||||
|
case oneMinusSrc1
|
||||||
|
case src1Alpha
|
||||||
|
case oneMinusSrc1Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BlendOperation: UInt32, Sendable, CaseIterable {
|
||||||
|
case add
|
||||||
|
case subtract
|
||||||
|
case reverseSubtract
|
||||||
|
case max
|
||||||
|
case min
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ColorTarget {
|
||||||
|
let colorBlendOperation: BlendOperation
|
||||||
|
let colorBlendSrcFactor: BlendFactor
|
||||||
|
let colorBlendDestFactor: BlendFactor
|
||||||
|
let alphaBlendOperation: BlendOperation
|
||||||
|
let alphaBlendSrcFactor: BlendFactor
|
||||||
|
let alphaBlendDestFactor: BlendFactor
|
||||||
|
let channels: Color.Channel
|
||||||
|
|
||||||
|
public init(
|
||||||
|
channels: Color.Channel,
|
||||||
|
colorBlendOperation: BlendOperation = .add,
|
||||||
|
colorBlendSrcFactor: BlendFactor = .one,
|
||||||
|
colorBlendDestFactor: BlendFactor = .zero,
|
||||||
|
alphaBlendOperation: BlendOperation = .add,
|
||||||
|
alphaBlendSrcFactor: BlendFactor = .one,
|
||||||
|
alphaBlendDestFactor: BlendFactor = .zero
|
||||||
|
) {
|
||||||
|
self.channels = channels
|
||||||
|
self.colorBlendOperation = colorBlendOperation
|
||||||
|
self.colorBlendSrcFactor = colorBlendSrcFactor
|
||||||
|
self.colorBlendDestFactor = colorBlendDestFactor
|
||||||
|
self.alphaBlendOperation = alphaBlendOperation
|
||||||
|
self.alphaBlendSrcFactor = alphaBlendSrcFactor
|
||||||
|
self.alphaBlendDestFactor = alphaBlendDestFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RenderPass {
|
||||||
|
var name: StaticString
|
||||||
|
var label: StaticString
|
||||||
|
var type: RenderPassType
|
||||||
|
var depends: [StaticString]
|
||||||
|
var bindings: [RenderPassBinding]
|
||||||
|
var vertexShader: Shader
|
||||||
|
var vertexLayout: [VertexBufferLayout]
|
||||||
|
var fragmentShader: Shader
|
||||||
|
var colorTargets: [ColorTarget]
|
||||||
|
var topology: PrimitiveTopology
|
||||||
|
var windingOrder: WindingOrder
|
||||||
|
var hasDepthStencil: Bool
|
||||||
|
var multisampling: Int
|
||||||
|
var clearColor: Color
|
||||||
|
|
||||||
|
public init(
|
||||||
|
name: StaticString,
|
||||||
|
label: StaticString? = nil,
|
||||||
|
type: RenderPassType = .graphics,
|
||||||
|
depends: [StaticString] = [],
|
||||||
|
bindings: [RenderPassBinding] = [],
|
||||||
|
vertexLayout: [VertexBufferLayout],
|
||||||
|
vertexShader: Shader,
|
||||||
|
colorTargets: [ColorTarget],
|
||||||
|
fragmentShader: Shader,
|
||||||
|
depthStencil: Bool? = nil,
|
||||||
|
multisampling: Int = 1,
|
||||||
|
topology: PrimitiveTopology = .triangles,
|
||||||
|
windingOrder: WindingOrder = .counterclockwise,
|
||||||
|
clearColor: Color
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.label = label ?? name
|
||||||
|
self.type = type
|
||||||
|
self.depends = depends
|
||||||
|
self.bindings = bindings
|
||||||
|
self.vertexShader = vertexShader
|
||||||
|
self.vertexLayout = vertexLayout
|
||||||
|
self.fragmentShader = fragmentShader
|
||||||
|
self.colorTargets = colorTargets
|
||||||
|
self.hasDepthStencil = depthStencil != nil
|
||||||
|
self.multisampling = multisampling
|
||||||
|
self.topology = topology
|
||||||
|
self.windingOrder = windingOrder
|
||||||
|
self.clearColor = clearColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
899
Sources/ArtifactGraphics/WebGPU.swift
Normal file
899
Sources/ArtifactGraphics/WebGPU.swift
Normal file
|
|
@ -0,0 +1,899 @@
|
||||||
|
|
||||||
|
#if WebGPU
|
||||||
|
import ArtifactMath
|
||||||
|
import ArtifactPlatform
|
||||||
|
import WGPUNative
|
||||||
|
|
||||||
|
public protocol WGPUCompatibleWindow: Window {
|
||||||
|
func getWGPUSurface(instance: WGPUInstance) -> WGPUSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
//#if canImport(GLFW)
|
||||||
|
//import glfw3webgpu
|
||||||
|
//
|
||||||
|
//extension GLFWWindow: WGPUCompatibleWindow {
|
||||||
|
// public func getWGPUSurface(instance: WGPUInstance) -> WGPUSurface {
|
||||||
|
// return glfwCreateWindowWGPUSurface(instance, window)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
private func stringView(_ string: StaticString) -> WGPUStringView {
|
||||||
|
string.withUTF8Buffer { ptr in
|
||||||
|
ptr.withMemoryRebound(to: CChar.self) { ptr in
|
||||||
|
WGPUStringView(data: ptr.baseAddress, length: string.utf8CodeUnitCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//private func stringView(retained string: String) -> WGPUStringView {
|
||||||
|
// return string.withCString { ptr in
|
||||||
|
// WGPUStringView(data: ptr, length: string.count)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
private actor StringViewStorage {
|
||||||
|
static var views: [[UInt8]] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func stringView(retaining string: String) -> WGPUStringView {
|
||||||
|
var bytes = Array(string.utf8)
|
||||||
|
StringViewStorage.views.append(bytes)
|
||||||
|
return bytes.withUnsafeBytes { ptr in
|
||||||
|
WGPUStringView(data: ptr.assumingMemoryBound(to: CChar.self).baseAddress, length: string.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BufferUsage {
|
||||||
|
var wgpuValue: WGPUFlags {
|
||||||
|
switch self {
|
||||||
|
case .vertex: WGPUBufferUsage_Vertex
|
||||||
|
case .index: WGPUBufferUsage_Index
|
||||||
|
case .uniform: WGPUBufferUsage_Uniform
|
||||||
|
case .storage: WGPUBufferUsage_Storage
|
||||||
|
case .indirect: WGPUBufferUsage_Indirect
|
||||||
|
|
||||||
|
case .copySrc: WGPUBufferUsage_CopySrc
|
||||||
|
case .copyDest: WGPUBufferUsage_CopyDst
|
||||||
|
case .mapRead: WGPUBufferUsage_MapRead
|
||||||
|
case .mapWrite: WGPUBufferUsage_MapWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebGPUBuffer: Buffer {
|
||||||
|
private let context: WebGPUGraphics
|
||||||
|
let handle: WGPUBuffer
|
||||||
|
public let size: UInt64
|
||||||
|
|
||||||
|
public init(context: WebGPUGraphics, label: String, usage: [BufferUsage], size: UInt64, mapped: Bool = false) {
|
||||||
|
self.context = context
|
||||||
|
self.size = size
|
||||||
|
var desc = WGPUBufferDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView(retaining: label),
|
||||||
|
usage: usage.reduce(0) { $0 | $1.wgpuValue },
|
||||||
|
size: size,
|
||||||
|
mappedAtCreation: mapped ? 1 : 0
|
||||||
|
)
|
||||||
|
handle = wgpuDeviceCreateBuffer(context.device, &desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func writeData(_ data: [UInt8]) {
|
||||||
|
data.withUnsafeBytes { ptr in
|
||||||
|
wgpuQueueWriteBuffer(context.queue, handle, 0, ptr.baseAddress, data.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension TextureUsage {
|
||||||
|
var wgpuValue: WGPUFlags {
|
||||||
|
switch self {
|
||||||
|
case .texture: WGPUTextureUsage_TextureBinding
|
||||||
|
case .storage: WGPUTextureUsage_StorageBinding
|
||||||
|
case .attachment: WGPUTextureUsage_RenderAttachment
|
||||||
|
|
||||||
|
case .copySrc: WGPUTextureUsage_CopySrc
|
||||||
|
case .copyDest: WGPUTextureUsage_CopyDst
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebGPUTexture: Texture {
|
||||||
|
private let context: WebGPUGraphics
|
||||||
|
let handle: WGPUTexture
|
||||||
|
let view: WGPUTextureView
|
||||||
|
public let width: UInt32
|
||||||
|
public let height: UInt32
|
||||||
|
public let depth: UInt32
|
||||||
|
public let dimensions: Int
|
||||||
|
|
||||||
|
init(context: WebGPUGraphics, label: String, usage: [TextureUsage], format: WGPUTextureFormat = WGPUTextureFormat_BGRA8Unorm, dimensions: Int = 2, size: [UInt32], mipLevels: UInt32 = 1, samples: UInt32 = 1) {
|
||||||
|
self.context = context
|
||||||
|
width = max(1, size[0])
|
||||||
|
height = max(1, size.count > 1 ? size[1] : 1)
|
||||||
|
depth = max(1, size.count > 2 ? size[2] : 1)
|
||||||
|
self.dimensions = dimensions
|
||||||
|
|
||||||
|
let usage = usage.reduce(0) { $0 | $1.wgpuValue }
|
||||||
|
var desc = WGPUTextureDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView(retaining: label),
|
||||||
|
usage: usage,
|
||||||
|
dimension: dimensions == 3 ? WGPUTextureDimension_3D : dimensions == 1 ? WGPUTextureDimension_1D : WGPUTextureDimension_2D,
|
||||||
|
size: WGPUExtent3D(width: width, height: height, depthOrArrayLayers: depth),
|
||||||
|
format: format,
|
||||||
|
mipLevelCount: mipLevels,
|
||||||
|
sampleCount: samples,
|
||||||
|
viewFormatCount: 0,
|
||||||
|
viewFormats: nil
|
||||||
|
)
|
||||||
|
handle = wgpuDeviceCreateTexture(context.device, &desc)
|
||||||
|
|
||||||
|
var viewDesc = WGPUTextureViewDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView(retaining: label),
|
||||||
|
format: format,
|
||||||
|
dimension: dimensions == 3 ? WGPUTextureViewDimension_3D : dimensions == 1 ? WGPUTextureViewDimension_1D : WGPUTextureViewDimension_2D,
|
||||||
|
baseMipLevel: 0,
|
||||||
|
mipLevelCount: mipLevels,
|
||||||
|
baseArrayLayer: 0,
|
||||||
|
arrayLayerCount: 1,
|
||||||
|
aspect: WGPUTextureAspect_All,
|
||||||
|
usage: usage
|
||||||
|
)
|
||||||
|
view = wgpuTextureCreateView(handle, &viewDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
wgpuTextureViewRelease(view)
|
||||||
|
wgpuTextureDestroy(handle)
|
||||||
|
wgpuTextureRelease(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func writeData(_ data: [UInt8]) {
|
||||||
|
var size = WGPUExtent3D(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
depthOrArrayLayers: depth
|
||||||
|
)
|
||||||
|
|
||||||
|
var layout = WGPUTexelCopyBufferLayout(
|
||||||
|
offset: 0,
|
||||||
|
bytesPerRow: width * 4,
|
||||||
|
rowsPerImage: height
|
||||||
|
)
|
||||||
|
|
||||||
|
var dest = WGPUTexelCopyTextureInfo(
|
||||||
|
texture: handle,
|
||||||
|
mipLevel: 0,
|
||||||
|
origin: WGPUOrigin3D(x: 0, y: 0, z: 0),
|
||||||
|
aspect: WGPUTextureAspect_All
|
||||||
|
)
|
||||||
|
|
||||||
|
data.withUnsafeBytes { ptr in
|
||||||
|
wgpuQueueWriteTexture(context.queue, &dest, ptr.baseAddress, data.count, &layout, &size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebGPUSampler: Sampler {
|
||||||
|
private let context: WebGPUGraphics
|
||||||
|
let handle: WGPUSampler
|
||||||
|
|
||||||
|
public init(context: WebGPUGraphics, label: String, addressMode: AddressMode = .clamp) {
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
var desc = WGPUSamplerDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView(retaining: label),
|
||||||
|
addressModeU: addressMode.wgpuValue,
|
||||||
|
addressModeV: addressMode.wgpuValue, // TODO: Allow configuring these separately.
|
||||||
|
addressModeW: addressMode.wgpuValue,
|
||||||
|
magFilter: WGPUFilterMode_Nearest, // TODO: Allow configuration.
|
||||||
|
minFilter: WGPUFilterMode_Nearest, // TODO: Allow configuration.
|
||||||
|
mipmapFilter: WGPUMipmapFilterMode_Linear, // TODO: Allow configuration.
|
||||||
|
lodMinClamp: 0, // TODO: Allow configuration.
|
||||||
|
lodMaxClamp: 0, // TODO: Allow configuration.
|
||||||
|
compare: WGPUCompareFunction_Undefined, // TODO: Allow configuration.
|
||||||
|
maxAnisotropy: 1 // TODO: Allow configuration.
|
||||||
|
)
|
||||||
|
handle = wgpuDeviceCreateSampler(context.device, &desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
wgpuSamplerRelease(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AddressMode {
|
||||||
|
var wgpuValue: WGPUAddressMode {
|
||||||
|
switch self {
|
||||||
|
case .repeat: WGPUAddressMode_Repeat
|
||||||
|
case .mirrorRepeat: WGPUAddressMode_MirrorRepeat
|
||||||
|
case .clamp: WGPUAddressMode_ClampToEdge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ShaderStage {
|
||||||
|
var wgpuValue: WGPUShaderStage {
|
||||||
|
switch self {
|
||||||
|
case .vertex: WGPUShaderStage_Vertex
|
||||||
|
case .fragment: WGPUShaderStage_Fragment
|
||||||
|
case .compute: WGPUShaderStage_Compute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BufferBindingType {
|
||||||
|
var wgpuValue: WGPUBufferBindingType {
|
||||||
|
switch self {
|
||||||
|
case .storage: WGPUBufferBindingType_Storage
|
||||||
|
case .readOnlyStorage: WGPUBufferBindingType_ReadOnlyStorage
|
||||||
|
case .uniform: WGPUBufferBindingType_Uniform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SamplerBindingType {
|
||||||
|
var wgpuValue: WGPUSamplerBindingType {
|
||||||
|
switch self {
|
||||||
|
case .comparison: WGPUSamplerBindingType_Comparison
|
||||||
|
case .filtering: WGPUSamplerBindingType_Filtering
|
||||||
|
case .nonfiltering: WGPUSamplerBindingType_NonFiltering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TextureSampleType {
|
||||||
|
var wgpuValue: WGPUTextureSampleType {
|
||||||
|
switch self {
|
||||||
|
case .float: WGPUTextureSampleType_Float
|
||||||
|
case .sint: WGPUTextureSampleType_Sint
|
||||||
|
case .uint: WGPUTextureSampleType_Uint
|
||||||
|
case .depth: WGPUTextureSampleType_Depth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PrimitiveTopology {
|
||||||
|
var wgpuValue: WGPUPrimitiveTopology {
|
||||||
|
switch self {
|
||||||
|
case .points: WGPUPrimitiveTopology_PointList
|
||||||
|
case .lines: WGPUPrimitiveTopology_LineList
|
||||||
|
case .lineStrip: WGPUPrimitiveTopology_LineStrip
|
||||||
|
case .triangles: WGPUPrimitiveTopology_TriangleList
|
||||||
|
case .triangleStrip: WGPUPrimitiveTopology_TriangleStrip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WindingOrder {
|
||||||
|
var wgpuValue: WGPUFrontFace {
|
||||||
|
switch self {
|
||||||
|
case .clockwise: WGPUFrontFace_CW
|
||||||
|
case .counterclockwise: WGPUFrontFace_CCW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VertexStepMode {
|
||||||
|
var wgpuValue: WGPUVertexStepMode {
|
||||||
|
switch self {
|
||||||
|
case .vertex: WGPUVertexStepMode_Vertex
|
||||||
|
case .instance: WGPUVertexStepMode_Instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VertexAttributeType {
|
||||||
|
var wgpuValue: WGPUVertexFormat {
|
||||||
|
switch self {
|
||||||
|
case .uint8: WGPUVertexFormat_Uint8
|
||||||
|
|
||||||
|
case .vec2f: WGPUVertexFormat_Float32x2
|
||||||
|
case .vec3f: WGPUVertexFormat_Float32x3
|
||||||
|
case .vec4f: WGPUVertexFormat_Float32x4
|
||||||
|
|
||||||
|
case .vec2n: WGPUVertexFormat_Unorm8x2
|
||||||
|
case .vec4n: WGPUVertexFormat_Unorm8x4
|
||||||
|
|
||||||
|
default:
|
||||||
|
fatalError("Unsupported vertex format: \(self)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BlendFactor {
|
||||||
|
var wgpuValue: WGPUBlendFactor {
|
||||||
|
switch self {
|
||||||
|
case .zero: WGPUBlendFactor_Zero
|
||||||
|
case .one: WGPUBlendFactor_One
|
||||||
|
case .src: WGPUBlendFactor_Src
|
||||||
|
case .oneMinusSrc: WGPUBlendFactor_OneMinusSrc
|
||||||
|
case .srcAlpha: WGPUBlendFactor_SrcAlpha
|
||||||
|
case .oneMinusSrcAlpha: WGPUBlendFactor_OneMinusSrcAlpha
|
||||||
|
case .dest: WGPUBlendFactor_Dst
|
||||||
|
case .oneMinusDest: WGPUBlendFactor_OneMinusDst
|
||||||
|
case .dstAlpha: WGPUBlendFactor_DstAlpha
|
||||||
|
case .oneMinusDstAlpha: WGPUBlendFactor_OneMinusDstAlpha
|
||||||
|
case .srcAlphaSaturated: WGPUBlendFactor_SrcAlphaSaturated
|
||||||
|
case .constant: WGPUBlendFactor_Constant
|
||||||
|
case .oneMinusConstant: WGPUBlendFactor_OneMinusConstant
|
||||||
|
case .src1: WGPUBlendFactor_Src1
|
||||||
|
case .oneMinusSrc1: WGPUBlendFactor_OneMinusSrc1
|
||||||
|
case .src1Alpha: WGPUBlendFactor_Src1Alpha
|
||||||
|
case .oneMinusSrc1Alpha: WGPUBlendFactor_OneMinusSrc1Alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BlendOperation {
|
||||||
|
var wgpuValue: WGPUBlendOperation {
|
||||||
|
switch self {
|
||||||
|
case .add: WGPUBlendOperation_Add
|
||||||
|
case .subtract: WGPUBlendOperation_Subtract
|
||||||
|
case .reverseSubtract: WGPUBlendOperation_ReverseSubtract
|
||||||
|
case .min: WGPUBlendOperation_Min
|
||||||
|
case .max: WGPUBlendOperation_Max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebGPUPipeline {
|
||||||
|
let context: WebGPUGraphics
|
||||||
|
var handle: WGPURenderPipeline!
|
||||||
|
var bindGroup: WGPUBindGroup!
|
||||||
|
private var vertexOutputs: [WGPUVertexBufferLayout] = []
|
||||||
|
private var blendStates: UnsafeMutablePointer<WGPUBlendState>?
|
||||||
|
private var targets: UnsafeMutablePointer<WGPUColorTargetState>?
|
||||||
|
private var fragmentState: WGPUFragmentState!
|
||||||
|
|
||||||
|
init(context: WebGPUGraphics, pass: RenderPass) {
|
||||||
|
self.context = context
|
||||||
|
generate(from: pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateLayout(from pass: RenderPass) -> WGPUBindGroupLayout {
|
||||||
|
var bindingNum: UInt32 = 0
|
||||||
|
var layouts: [WGPUBindGroupLayoutEntry] = []
|
||||||
|
var entries: [WGPUBindGroupEntry] = []
|
||||||
|
for item in pass.bindings {
|
||||||
|
switch item {
|
||||||
|
case .buffer(let stage, let type, let buffer):
|
||||||
|
layouts.append(WGPUBindGroupLayoutEntry(
|
||||||
|
nextInChain: nil,
|
||||||
|
binding: bindingNum,
|
||||||
|
visibility: stage.wgpuValue,
|
||||||
|
buffer: WGPUBufferBindingLayout(nextInChain: nil, type: type.wgpuValue, hasDynamicOffset: 0, minBindingSize: 0),
|
||||||
|
sampler: WGPUSamplerBindingLayout(),
|
||||||
|
texture: WGPUTextureBindingLayout(),
|
||||||
|
storageTexture: WGPUStorageTextureBindingLayout()
|
||||||
|
))
|
||||||
|
entries.append(WGPUBindGroupEntry(
|
||||||
|
nextInChain: nil,
|
||||||
|
binding: bindingNum,
|
||||||
|
buffer: (buffer as! WebGPUBuffer).handle,
|
||||||
|
offset: 0, // TODO: Allow configuration. (This is the byte offset of the binding range for the buffer.)
|
||||||
|
size: buffer.size,
|
||||||
|
sampler: nil,
|
||||||
|
textureView: nil
|
||||||
|
))
|
||||||
|
case .sampler(let stage, let type, let sampler):
|
||||||
|
layouts.append(WGPUBindGroupLayoutEntry(
|
||||||
|
nextInChain: nil,
|
||||||
|
binding: bindingNum,
|
||||||
|
visibility: stage.wgpuValue,
|
||||||
|
buffer: WGPUBufferBindingLayout(),
|
||||||
|
sampler: WGPUSamplerBindingLayout(nextInChain: nil, type: type.wgpuValue),
|
||||||
|
texture: WGPUTextureBindingLayout(),
|
||||||
|
storageTexture: WGPUStorageTextureBindingLayout()
|
||||||
|
))
|
||||||
|
entries.append(WGPUBindGroupEntry(
|
||||||
|
nextInChain: nil,
|
||||||
|
binding: bindingNum,
|
||||||
|
buffer: nil,
|
||||||
|
offset: 0,
|
||||||
|
size: 0,
|
||||||
|
sampler: (sampler as! WebGPUSampler).handle,
|
||||||
|
textureView: nil
|
||||||
|
))
|
||||||
|
case .texture(let stage, let sampleType, let texture):
|
||||||
|
layouts.append(WGPUBindGroupLayoutEntry(
|
||||||
|
nextInChain: nil,
|
||||||
|
binding: bindingNum,
|
||||||
|
visibility: stage.wgpuValue,
|
||||||
|
buffer: WGPUBufferBindingLayout(),
|
||||||
|
sampler: WGPUSamplerBindingLayout(),
|
||||||
|
texture: WGPUTextureBindingLayout(nextInChain: nil, sampleType: sampleType.wgpuValue, viewDimension: texture.dimensions == 3 ? WGPUTextureViewDimension_3D : texture.dimensions == 1 ? WGPUTextureViewDimension_1D : WGPUTextureViewDimension_2D, multisampled: 0), // TODO: Multisample?
|
||||||
|
storageTexture: WGPUStorageTextureBindingLayout()
|
||||||
|
))
|
||||||
|
entries.append(WGPUBindGroupEntry(
|
||||||
|
nextInChain: nil,
|
||||||
|
binding: bindingNum,
|
||||||
|
buffer: nil,
|
||||||
|
offset: 0,
|
||||||
|
size: 0,
|
||||||
|
sampler: nil,
|
||||||
|
textureView: (texture as! WebGPUTexture).view
|
||||||
|
))
|
||||||
|
}
|
||||||
|
bindingNum += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var bindGroupLayout = layouts.withUnsafeBufferPointer { layoutsPtr in
|
||||||
|
var bindGroupLayoutDesc = WGPUBindGroupLayoutDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView("."),
|
||||||
|
entryCount: layouts.count,
|
||||||
|
entries: layoutsPtr.baseAddress
|
||||||
|
)
|
||||||
|
return wgpuDeviceCreateBindGroupLayout(context.device, &bindGroupLayoutDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindGroup = entries.withUnsafeBufferPointer { entries in
|
||||||
|
var bindGroupDesc = WGPUBindGroupDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView(retaining: "\(pass.name) - bind group"),
|
||||||
|
layout: bindGroupLayout,
|
||||||
|
entryCount: entries.count,
|
||||||
|
entries: entries.baseAddress
|
||||||
|
)
|
||||||
|
return wgpuDeviceCreateBindGroup(context.device, &bindGroupDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return withUnsafePointer(to: bindGroupLayout) { bindGroupLayout in
|
||||||
|
var layoutDesc = WGPUPipelineLayoutDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView(retaining: "\(pass.name) - layout"),
|
||||||
|
bindGroupLayoutCount: 1, // TODO: Multiple layouts?
|
||||||
|
bindGroupLayouts: bindGroupLayout
|
||||||
|
)
|
||||||
|
return wgpuDeviceCreatePipelineLayout(context.device, &layoutDesc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateShaderModule(code: String) -> WGPUShaderModule {
|
||||||
|
withExtendedLifetime(WGPUChainedStruct(next: nil, sType: WGPUSType_ShaderSourceWGSL)) { chain in // TODO: Support multiple languages.
|
||||||
|
var source = WGPUShaderSourceWGSL(chain: chain, code: stringView(retaining: code))
|
||||||
|
return withUnsafePointer(to: chain) { chain in
|
||||||
|
var desc = WGPUShaderModuleDescriptor(nextInChain: chain, label: stringView("Shader"))
|
||||||
|
return wgpuDeviceCreateShaderModule(context.device, &desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateShaders(from pass: RenderPass) -> (vertexState: WGPUVertexState, buffers: [WGPUVertexBufferLayout], vertexAttributes: [[WGPUVertexAttribute]]) {
|
||||||
|
var vertexShader = generateShaderModule(code: pass.vertexShader.source)
|
||||||
|
|
||||||
|
var constants = pass.vertexShader.constants.map { WGPUConstantEntry(nextInChain: nil, key: stringView(retaining: $0), value: $1) }
|
||||||
|
|
||||||
|
// This is needed to extend the lifetime of the attribute lists.
|
||||||
|
var attributeStore: [[WGPUVertexAttribute]] = []
|
||||||
|
var outputs = pass.vertexLayout.map { layout in
|
||||||
|
var num: UInt32 = 0
|
||||||
|
var offset: UInt64 = 0
|
||||||
|
var attributes = layout.attributes.map { item in
|
||||||
|
defer {
|
||||||
|
num += 1
|
||||||
|
offset += UInt64(item.size)
|
||||||
|
}
|
||||||
|
return WGPUVertexAttribute(
|
||||||
|
format: item.wgpuValue,
|
||||||
|
offset: offset,
|
||||||
|
shaderLocation: num
|
||||||
|
)
|
||||||
|
}
|
||||||
|
attributeStore.append(attributes)
|
||||||
|
return attributes.withUnsafeBufferPointer { attributes in
|
||||||
|
return WGPUVertexBufferLayout(stepMode: layout.stepMode.wgpuValue, arrayStride: offset, attributeCount: layout.attributes.count, attributes: attributes.baseAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var vertexState = constants.withUnsafeBufferPointer { constants in
|
||||||
|
outputs.withUnsafeBufferPointer { outputs in
|
||||||
|
WGPUVertexState(
|
||||||
|
nextInChain: nil,
|
||||||
|
module: vertexShader,
|
||||||
|
entryPoint: stringView(pass.vertexShader.entryPoint),
|
||||||
|
constantCount: constants.count,
|
||||||
|
constants: constants.baseAddress,
|
||||||
|
bufferCount: outputs.count,
|
||||||
|
buffers: outputs.baseAddress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fragmentShader = generateShaderModule(code: pass.fragmentShader.source)
|
||||||
|
|
||||||
|
let fragmentConstants = pass.fragmentShader.constants.map { WGPUConstantEntry(nextInChain: nil, key: stringView(retaining: $0), value: $1) }
|
||||||
|
|
||||||
|
if pass.colorTargets.count > 0 {
|
||||||
|
// We manually allocate memory here to ensure that the pointer remains stable until we need to use it.
|
||||||
|
blendStates = UnsafeMutablePointer<WGPUBlendState>.allocate(capacity: pass.colorTargets.count)
|
||||||
|
targets = UnsafeMutablePointer<WGPUColorTargetState>.allocate(capacity: pass.colorTargets.count)
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
for item in pass.colorTargets {
|
||||||
|
blendStates![i] = WGPUBlendState(
|
||||||
|
color: WGPUBlendComponent(
|
||||||
|
operation: item.colorBlendOperation.wgpuValue,
|
||||||
|
srcFactor: item.colorBlendSrcFactor.wgpuValue,
|
||||||
|
dstFactor: item.colorBlendDestFactor.wgpuValue
|
||||||
|
),
|
||||||
|
alpha: WGPUBlendComponent(
|
||||||
|
operation: item.alphaBlendOperation.wgpuValue,
|
||||||
|
srcFactor: item.alphaBlendSrcFactor.wgpuValue,
|
||||||
|
dstFactor: item.alphaBlendDestFactor.wgpuValue
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let mask = switch item.channels {
|
||||||
|
case .all: WGPUColorWriteMask_All
|
||||||
|
case .none: WGPUColorWriteMask_None
|
||||||
|
case .r: WGPUColorWriteMask_Red
|
||||||
|
case .g: WGPUColorWriteMask_Green
|
||||||
|
case .b: WGPUColorWriteMask_Blue
|
||||||
|
case .a: WGPUColorWriteMask_Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
targets![i] = WGPUColorTargetState(
|
||||||
|
nextInChain: nil,
|
||||||
|
format: WGPUTextureFormat_BGRA8Unorm, // TODO: Allow configuration.
|
||||||
|
blend: blendStates!.advanced(by: i),
|
||||||
|
writeMask: mask
|
||||||
|
)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentState = fragmentConstants.withUnsafeBufferPointer { constants in
|
||||||
|
WGPUFragmentState(
|
||||||
|
nextInChain: nil,
|
||||||
|
module: fragmentShader,
|
||||||
|
entryPoint: stringView(pass.fragmentShader.entryPoint),
|
||||||
|
constantCount: constants.count,
|
||||||
|
constants: constants.baseAddress,
|
||||||
|
targetCount: pass.colorTargets.count,
|
||||||
|
targets: targets
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (vertexState, outputs, attributeStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(from pass: RenderPass) {
|
||||||
|
let layout = generateLayout(from: pass)
|
||||||
|
let shaders = generateShaders(from: pass)
|
||||||
|
|
||||||
|
let primitiveState = WGPUPrimitiveState(
|
||||||
|
nextInChain: nil,
|
||||||
|
topology: pass.topology.wgpuValue,
|
||||||
|
stripIndexFormat: pass.topology == .lineStrip || pass.topology == .triangleStrip ? WGPUIndexFormat_Uint32 : WGPUIndexFormat_Undefined, // TODO: Allow configuration.
|
||||||
|
frontFace: pass.windingOrder.wgpuValue,
|
||||||
|
cullMode: WGPUCullMode_Back, // TODO: Allow configuration.
|
||||||
|
unclippedDepth: 0 // TODO: Allow configuration.
|
||||||
|
)
|
||||||
|
|
||||||
|
let depthStencil: WGPUDepthStencilState? = if pass.hasDepthStencil {
|
||||||
|
WGPUDepthStencilState( // TODO: Allow configuration.
|
||||||
|
nextInChain: nil,
|
||||||
|
format: WGPUTextureFormat_Depth32Float,
|
||||||
|
depthWriteEnabled: WGPUOptionalBool_True,
|
||||||
|
depthCompare: WGPUCompareFunction_Less,
|
||||||
|
stencilFront: WGPUStencilFaceState(),
|
||||||
|
stencilBack: WGPUStencilFaceState(),
|
||||||
|
stencilReadMask: 0,
|
||||||
|
stencilWriteMask: 0,
|
||||||
|
depthBias: 0,
|
||||||
|
depthBiasSlopeScale: 0,
|
||||||
|
depthBiasClamp: 0
|
||||||
|
)
|
||||||
|
} else { nil }
|
||||||
|
|
||||||
|
let multisampleState = WGPUMultisampleState( // TODO: Allow configuration.
|
||||||
|
nextInChain: nil,
|
||||||
|
count: UInt32(pass.multisampling),
|
||||||
|
mask: 0,
|
||||||
|
alphaToCoverageEnabled: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
withUnsafePointer(to: fragmentState!) { fragmentState in
|
||||||
|
let cb: (UnsafePointer?) -> Void = { [self] depthStencil in
|
||||||
|
var desc = WGPURenderPipelineDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView(pass.name),
|
||||||
|
layout: layout,
|
||||||
|
vertex: shaders.vertexState,
|
||||||
|
primitive: primitiveState,
|
||||||
|
depthStencil: depthStencil,
|
||||||
|
multisample: multisampleState,
|
||||||
|
fragment: fragmentState
|
||||||
|
)
|
||||||
|
|
||||||
|
handle = wgpuDeviceCreateRenderPipeline(context.device, &desc)
|
||||||
|
}
|
||||||
|
if let depthStencil = depthStencil {
|
||||||
|
withUnsafePointer(to: depthStencil) { ptr in
|
||||||
|
cb(ptr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cb(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blendStates?.deallocate()
|
||||||
|
targets?.deallocate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebGPUGraphics: Graphics {
|
||||||
|
var passes: [RenderPass] = []
|
||||||
|
var viewportWidth: Int
|
||||||
|
var viewportHeight: Int
|
||||||
|
public var scene: [Drawable] = []
|
||||||
|
|
||||||
|
let instance: WGPUInstance
|
||||||
|
var adapter: WGPUAdapter!
|
||||||
|
var device: WGPUDevice!
|
||||||
|
var surface: WGPUSurface!
|
||||||
|
var queue: WGPUQueue!
|
||||||
|
var pipelines: [String: WebGPUPipeline] = [:]
|
||||||
|
var antialiased: WebGPUTexture!
|
||||||
|
|
||||||
|
private var _depthTexture = false
|
||||||
|
lazy var depthTexture: WebGPUTexture = {
|
||||||
|
_depthTexture = true
|
||||||
|
return .init(context: self, label: "Depth texture", usage: [.attachment], format: WGPUTextureFormat_Depth32Float, size: [UInt32(viewportWidth), UInt32(viewportHeight)]) // TODO: Adapt to view size.
|
||||||
|
}()
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public required convenience init(_ window: any Window) {
|
||||||
|
assert(window is any WGPUCompatibleWindow, "The window is not capable of providing a WebGPU surface.")
|
||||||
|
self.init(window as! any WGPUCompatibleWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
init(_ window: any WGPUCompatibleWindow) {
|
||||||
|
viewportWidth = window.width
|
||||||
|
viewportHeight = window.height
|
||||||
|
|
||||||
|
instance = wgpuCreateInstance(nil)
|
||||||
|
var adapterOptions = WGPURequestAdapterOptions()
|
||||||
|
wgpuInstanceRequestAdapter(instance, &adapterOptions, WGPURequestAdapterCallbackInfo(nextInChain: nil, mode: WGPUCallbackMode_AllowProcessEvents, callback: { status, adapter, err, userdata, _ in
|
||||||
|
if status == WGPURequestAdapterStatus_Success {
|
||||||
|
let me = Unmanaged<WebGPUGraphics>.fromOpaque(userdata!).takeUnretainedValue()
|
||||||
|
me.adapter = adapter
|
||||||
|
} else {
|
||||||
|
fatalError("Failed to initialize WebGPU: Adapter request error (\(status)): \(String(cString: err.data))")
|
||||||
|
}
|
||||||
|
}, userdata1: Unmanaged.passUnretained(self).toOpaque(), userdata2: nil))
|
||||||
|
while adapter == nil {
|
||||||
|
wgpuInstanceProcessEvents(instance)
|
||||||
|
}
|
||||||
|
var deviceOptions = WGPUDeviceDescriptor()
|
||||||
|
wgpuAdapterRequestDevice(adapter, &deviceOptions, WGPURequestDeviceCallbackInfo(nextInChain: nil, mode: WGPUCallbackMode_AllowProcessEvents, callback: { status, device, err, userdata, _ in
|
||||||
|
if status == WGPURequestDeviceStatus_Success {
|
||||||
|
let me = Unmanaged<WebGPUGraphics>.fromOpaque(userdata!).takeUnretainedValue()
|
||||||
|
me.device = device
|
||||||
|
} else {
|
||||||
|
fatalError("Failed to initialize WebGPU: Device request error (\(status)): \(String(cString: err.data))")
|
||||||
|
}
|
||||||
|
}, userdata1: Unmanaged.passUnretained(self).toOpaque(), userdata2: nil))
|
||||||
|
while device == nil {
|
||||||
|
wgpuInstanceProcessEvents(instance)
|
||||||
|
}
|
||||||
|
surface = window.getWGPUSurface(instance: instance)
|
||||||
|
|
||||||
|
resizeViewport(width: window.width, height: window.height)
|
||||||
|
|
||||||
|
window.onResize(resizeViewport)
|
||||||
|
|
||||||
|
window.onDraw(draw)
|
||||||
|
|
||||||
|
queue = wgpuDeviceGetQueue(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
wgpuQueueRelease(queue)
|
||||||
|
wgpuSurfaceRelease(surface)
|
||||||
|
wgpuDeviceDestroy(device)
|
||||||
|
wgpuDeviceRelease(device)
|
||||||
|
wgpuAdapterRelease(adapter)
|
||||||
|
wgpuInstanceRelease(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func resizeViewport(width: Int, height: Int) {
|
||||||
|
viewportWidth = width
|
||||||
|
viewportHeight = height
|
||||||
|
|
||||||
|
wgpuSurfaceUnconfigure(surface)
|
||||||
|
|
||||||
|
var config = WGPUSurfaceConfiguration(
|
||||||
|
nextInChain: nil,
|
||||||
|
device: device,
|
||||||
|
format: WGPUTextureFormat_BGRA8Unorm, // TODO: Allow configuration.
|
||||||
|
usage: WGPUTextureUsage_RenderAttachment,
|
||||||
|
width: UInt32(viewportWidth),
|
||||||
|
height: UInt32(viewportHeight),
|
||||||
|
viewFormatCount: 0,
|
||||||
|
viewFormats: nil,
|
||||||
|
alphaMode: WGPUCompositeAlphaMode_Auto,
|
||||||
|
presentMode: WGPUPresentMode_Fifo
|
||||||
|
)
|
||||||
|
wgpuSurfaceConfigure(surface, &config)
|
||||||
|
|
||||||
|
if antialiased != nil {
|
||||||
|
antialiased = WebGPUTexture(context: self, label: "MSAA intermediate texture", usage: [.copyDest, .attachment], size: [UInt32(viewportWidth), UInt32(viewportHeight)])
|
||||||
|
}
|
||||||
|
|
||||||
|
if _depthTexture {
|
||||||
|
depthTexture = .init(context: self, label: "Depth texture", usage: [.attachment], format: WGPUTextureFormat_Depth32Float, size: [UInt32(viewportWidth), UInt32(viewportHeight)]) // TODO: Adapt to view size.
|
||||||
|
}
|
||||||
|
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func createBuffer(label: String, usage: [BufferUsage], size: Int) -> any Buffer {
|
||||||
|
WebGPUBuffer(context: self, label: label, usage: usage, size: UInt64(size))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func createTexture(label: String, usage: [TextureUsage], size: [UInt32]) -> any Texture {
|
||||||
|
WebGPUTexture(context: self, label: label, usage: usage, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func createSampler(label: String) -> any Sampler {
|
||||||
|
WebGPUSampler(context: self, label: label, addressMode: .clamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func reload() {
|
||||||
|
for pass in passes {
|
||||||
|
pipelines["\(pass.name)"]?.generate(from: pass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func draw(alpha: Double) {
|
||||||
|
var surfaceTexture = WGPUSurfaceTexture()
|
||||||
|
wgpuSurfaceGetCurrentTexture(surface, &surfaceTexture)
|
||||||
|
if (surfaceTexture.status != WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal && surfaceTexture.status != WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal || surfaceTexture.texture == nil) {
|
||||||
|
print("Failed to get surface texture")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let nextTexture = wgpuTextureCreateView(surfaceTexture.texture, nil)
|
||||||
|
if nextTexture == nil {
|
||||||
|
print("Failed to create texture view")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let encoder = wgpuDeviceCreateCommandEncoder(device, nil)
|
||||||
|
|
||||||
|
for pass in passes {
|
||||||
|
var colorAttachment = WGPURenderPassColorAttachment(
|
||||||
|
nextInChain: nil,
|
||||||
|
view: nextTexture,
|
||||||
|
depthSlice: WGPU_DEPTH_SLICE_UNDEFINED,
|
||||||
|
resolveTarget: nil,
|
||||||
|
loadOp: WGPULoadOp_Clear,
|
||||||
|
storeOp: WGPUStoreOp_Store,
|
||||||
|
clearValue: WGPUColor(r: Double(pass.clearColor.r) / 255, g: Double(pass.clearColor.g) / 255, b: Double(pass.clearColor.b) / 255, a: Double(pass.clearColor.a) / 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
if pass.multisampling > 1 {
|
||||||
|
if antialiased == nil {
|
||||||
|
antialiased = WebGPUTexture(context: self, label: "MSAA intermediate texture", usage: [.copyDest, .attachment], size: [UInt32(viewportWidth), UInt32(viewportHeight)], samples: UInt32(pass.multisampling))
|
||||||
|
}
|
||||||
|
colorAttachment.view = antialiased.view
|
||||||
|
colorAttachment.resolveTarget = nextTexture
|
||||||
|
colorAttachment.storeOp = WGPUStoreOp_Discard
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderPass = withUnsafePointer(to: colorAttachment) { colorAttachment in
|
||||||
|
let cb: (UnsafePointer<WGPURenderPassDepthStencilAttachment>?) -> WGPURenderPassEncoder = { depthAttachment in
|
||||||
|
var renderPassDesc = WGPURenderPassDescriptor(
|
||||||
|
nextInChain: nil,
|
||||||
|
label: stringView(pass.label),
|
||||||
|
colorAttachmentCount: 1,
|
||||||
|
colorAttachments: colorAttachment,
|
||||||
|
depthStencilAttachment: depthAttachment,
|
||||||
|
occlusionQuerySet: nil,
|
||||||
|
timestampWrites: nil
|
||||||
|
)
|
||||||
|
return wgpuCommandEncoderBeginRenderPass(encoder, &renderPassDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pass.hasDepthStencil {
|
||||||
|
let depthAttachment = WGPURenderPassDepthStencilAttachment( // TODO: Allow configuration.
|
||||||
|
view: depthTexture.view,
|
||||||
|
depthLoadOp: WGPULoadOp_Clear,
|
||||||
|
depthStoreOp: WGPUStoreOp_Store,
|
||||||
|
depthClearValue: 1,
|
||||||
|
depthReadOnly: 0,
|
||||||
|
stencilLoadOp: WGPULoadOp_Undefined,
|
||||||
|
stencilStoreOp: WGPUStoreOp_Undefined,
|
||||||
|
stencilClearValue: 0,
|
||||||
|
stencilReadOnly: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return withUnsafePointer(to: depthAttachment) { depthAttachment in
|
||||||
|
cb(depthAttachment)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return cb(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pipeline = pipelines["\(pass.name)"]!
|
||||||
|
|
||||||
|
wgpuRenderPassEncoderSetPipeline(renderPass, pipeline.handle)
|
||||||
|
wgpuRenderPassEncoderSetViewport(renderPass, 0, 0, Float(viewportWidth), Float(viewportHeight), 0, 1)
|
||||||
|
wgpuRenderPassEncoderSetBindGroup(renderPass, 0, pipeline.bindGroup, 0, nil)
|
||||||
|
|
||||||
|
for item in scene {
|
||||||
|
if item.passes != nil && !item.passes!.contains("\(pass.name)") { continue }
|
||||||
|
wgpuRenderPassEncoderSetVertexBuffer(renderPass, 0, (item.vertices as! WebGPUBuffer).handle, 0, item.vertices.size)
|
||||||
|
|
||||||
|
for binding in pass.bindings {
|
||||||
|
if case .buffer(_, .uniform, let uniformBuffer) = binding {
|
||||||
|
if let data = item.uniforms {
|
||||||
|
uniformBuffer.writeData(data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.indices != nil {
|
||||||
|
wgpuRenderPassEncoderSetIndexBuffer(renderPass, (item.indices! as! WebGPUBuffer).handle, WGPUIndexFormat_Uint32, 0, item.indices!.size)
|
||||||
|
|
||||||
|
var offset: Int = 0
|
||||||
|
var scissor: [Rect<Float>] = []
|
||||||
|
var transform: [Mat4f] = []
|
||||||
|
for op in item.operations ?? [.draw(count: item.indexCount ?? item.vertexCount)] {
|
||||||
|
switch op {
|
||||||
|
case .draw(let count):
|
||||||
|
wgpuRenderPassEncoderDrawIndexed(renderPass, UInt32(count), 1, UInt32(offset), 0, 0)
|
||||||
|
offset += count
|
||||||
|
case .pushScissor(let rect):
|
||||||
|
wgpuRenderPassEncoderSetScissorRect(renderPass, UInt32(rect.left), UInt32(rect.top), UInt32(rect.width), UInt32(rect.height))
|
||||||
|
scissor.append(rect)
|
||||||
|
case .popScissor:
|
||||||
|
scissor.removeLast()
|
||||||
|
let rect = scissor.last ?? Rect(origin: Vec2f(0, 0), size: Vec2f(viewportWidth, viewportHeight))
|
||||||
|
wgpuRenderPassEncoderSetScissorRect(renderPass, UInt32(rect.left), UInt32(rect.top), UInt32(rect.width), UInt32(rect.height))
|
||||||
|
case .pushTransform(let transformation):
|
||||||
|
transform.append(transformation)
|
||||||
|
case .popTransform:
|
||||||
|
transform.removeLast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wgpuRenderPassEncoderDraw(renderPass, UInt32(item.vertexCount), 1, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wgpuRenderPassEncoderEnd(renderPass)
|
||||||
|
wgpuRenderPassEncoderRelease(renderPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandBufferDesc = WGPUCommandBufferDescriptor(nextInChain: nil, label: stringView("Command buffer"))
|
||||||
|
var cmdBuffer = wgpuCommandEncoderFinish(encoder, &commandBufferDesc)
|
||||||
|
wgpuQueueSubmit(queue, 1, &cmdBuffer)
|
||||||
|
wgpuSurfacePresent(surface) // TODO: Remove for wasm builds?
|
||||||
|
wgpuTextureViewRelease(nextTexture)
|
||||||
|
wgpuTextureRelease(surfaceTexture.texture)
|
||||||
|
wgpuCommandBufferRelease(cmdBuffer)
|
||||||
|
wgpuCommandEncoderRelease(encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func registerPass(_ def: RenderPass) {
|
||||||
|
passes.append(def)
|
||||||
|
let pipeline = WebGPUPipeline(context: self, pass: def)
|
||||||
|
pipelines["\(def.name)"] = pipeline
|
||||||
|
passes.sort { a, b in
|
||||||
|
b.depends.contains { "\($0)" == "\(a.name)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
12
Tests/ArtifactGraphicsTests/ArtifactGraphicsTests.swift
Normal file
12
Tests/ArtifactGraphicsTests/ArtifactGraphicsTests.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import ArtifactGraphics
|
||||||
|
|
||||||
|
final class ArtifactGraphicsTests: XCTestCase {
|
||||||
|
func testExample() throws {
|
||||||
|
// XCTest Documentation
|
||||||
|
// https://developer.apple.com/documentation/xctest
|
||||||
|
|
||||||
|
// Defining Test Cases and Test Methods
|
||||||
|
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue