Initial Commit

This commit is contained in:
Signal 2026-06-22 16:21:43 -04:00
commit b50da2cb27
6 changed files with 1298 additions and 0 deletions

8
.gitignore vendored Normal file
View 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
View 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"]
),
]
)

View 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
}

View 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
}
}

View 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

View 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
}
}