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