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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions Sources/UntoldEngine/AssetFormat/UntoldBinaryCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,12 @@ extension UntoldChunkEntryV1: UntoldBinaryEncodable, UntoldBinaryDecodable {
public static func decode(from reader: UntoldBinaryReader) throws -> UntoldChunkEntryV1 {
let chunkTypeRaw = try reader.readUInt32LE()
let compressionRaw = try reader.readUInt32LE()
guard let chunkType = UntoldChunkType(rawValue: chunkTypeRaw),
let compressionType = UntoldCompressionType(rawValue: compressionRaw)
else {
guard let compressionType = UntoldCompressionType(rawValue: compressionRaw) else {
throw UntoldValidationError.unsupportedEnumValue
}

var entry = try UntoldChunkEntryV1(
chunkType: chunkType,
chunkType: UntoldChunkType(rawValue: chunkTypeRaw),
compressionType: compressionType,
fileOffset: reader.readUInt64LE(),
compressedSize: reader.readUInt64LE(),
Expand Down
56 changes: 34 additions & 22 deletions Sources/UntoldEngine/AssetFormat/UntoldFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,40 @@ public enum UntoldFileType: UInt32, Sendable {
case animation = 5
}

public enum UntoldChunkType: UInt32, Sendable {
case stringTable = 1
case entityTable = 2
case meshTable = 3
case materialTable = 4
case textureTable = 5
case vertexData = 6
case indexData = 7
case skeletonTable = 8
case skeletonJointTable = 9
case skinTable = 10
case skinJointMappingTable = 11
case animationClipTable = 12
case animationChannelTable = 13
case translationKeyframeTable = 14
case rotationKeyframeTable = 15
case jointIndexData = 16
case jointWeightData = 17
case edgeIndexData = 18
case lightTable = 19
case cameraTable = 20
case colorManagementTable = 21
public struct UntoldChunkType: RawRepresentable, Hashable, Sendable, Equatable {
public let rawValue: UInt32

public init(rawValue: UInt32) {
self.rawValue = rawValue
}

public static let stringTable = UntoldChunkType(rawValue: 1)
public static let entityTable = UntoldChunkType(rawValue: 2)
public static let meshTable = UntoldChunkType(rawValue: 3)
public static let materialTable = UntoldChunkType(rawValue: 4)
public static let textureTable = UntoldChunkType(rawValue: 5)
public static let vertexData = UntoldChunkType(rawValue: 6)
public static let indexData = UntoldChunkType(rawValue: 7)
public static let skeletonTable = UntoldChunkType(rawValue: 8)
public static let skeletonJointTable = UntoldChunkType(rawValue: 9)
public static let skinTable = UntoldChunkType(rawValue: 10)
public static let skinJointMappingTable = UntoldChunkType(rawValue: 11)
public static let animationClipTable = UntoldChunkType(rawValue: 12)
public static let animationChannelTable = UntoldChunkType(rawValue: 13)
public static let translationKeyframeTable = UntoldChunkType(rawValue: 14)
public static let rotationKeyframeTable = UntoldChunkType(rawValue: 15)
public static let jointIndexData = UntoldChunkType(rawValue: 16)
public static let jointWeightData = UntoldChunkType(rawValue: 17)
public static let edgeIndexData = UntoldChunkType(rawValue: 18)
public static let lightTable = UntoldChunkType(rawValue: 19)
public static let cameraTable = UntoldChunkType(rawValue: 20)
public static let colorManagementTable = UntoldChunkType(rawValue: 21)

public static let firstPluginChunkRawValue: UInt32 = 0x8000

public var isPluginExtensionChunk: Bool {
rawValue >= Self.firstPluginChunkRawValue
}
}

public enum UntoldCompressionType: UInt32, Sendable {
Expand Down
95 changes: 95 additions & 0 deletions Sources/UntoldEngine/AssetFormat/UntoldPluginChunks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// UntoldPluginChunks.swift
// UntoldEngine
//
// Copyright (C) Untold Engine Studios
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import Foundation

public enum UntoldPluginChunkFormat {
public static let magic: UInt32 = 0x5458_4555 // "UEXT" little-endian bytes.
public static let version: UInt16 = 1
}

public struct UntoldPluginChunkMetadata: Sendable, Equatable {
public let pluginID: String
public let chunkKind: UInt32
public let chunkVersion: UInt32

public init(pluginID: String, chunkKind: UInt32, chunkVersion: UInt32) {
self.pluginID = pluginID
self.chunkKind = chunkKind
self.chunkVersion = chunkVersion
}
}

public struct UntoldPluginChunk: Sendable, Equatable {
public let chunkType: UntoldChunkType
public let metadata: UntoldPluginChunkMetadata
public let payload: Data

public init(
chunkType: UntoldChunkType,
metadata: UntoldPluginChunkMetadata,
payload: Data
) {
self.chunkType = chunkType
self.metadata = metadata
self.payload = payload
}
}

public enum UntoldPluginChunkEnvelope {
public static func encode(metadata: UntoldPluginChunkMetadata, payload: Data) -> Data {
let pluginIDData = Data(metadata.pluginID.utf8)
precondition(pluginIDData.count <= Int(UInt16.max), "Plugin IDs must fit in UInt16 byte length")

let writer = UntoldBinaryWriter()
writer.writeUInt32LE(UntoldPluginChunkFormat.magic)
writer.writeUInt16LE(UntoldPluginChunkFormat.version)
writer.writeUInt16LE(UInt16(pluginIDData.count))
writer.writeUInt32LE(metadata.chunkKind)
writer.writeUInt32LE(metadata.chunkVersion)
writer.writeData(pluginIDData)
writer.writeData(payload)
return writer.data
}

public static func decode(chunkType: UntoldChunkType, data: Data) throws -> UntoldPluginChunk {
let reader = UntoldBinaryReader(data: data)
let magic = try reader.readUInt32LE()
guard magic == UntoldPluginChunkFormat.magic else {
throw UntoldValidationError.invalidPluginChunkHeader
}

let version = try reader.readUInt16LE()
guard version == UntoldPluginChunkFormat.version else {
throw UntoldValidationError.unsupportedPluginChunkVersion(UInt32(version))
}

let pluginIDLength = try Int(reader.readUInt16LE())
let chunkKind = try reader.readUInt32LE()
let chunkVersion = try reader.readUInt32LE()
let pluginIDData = try reader.readBytes(count: pluginIDLength)
guard let pluginID = String(data: pluginIDData, encoding: .utf8),
!pluginID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
else {
throw UntoldValidationError.invalidPluginChunkHeader
}

let payload = try reader.readBytes(count: reader.remainingBytes)
return UntoldPluginChunk(
chunkType: chunkType,
metadata: UntoldPluginChunkMetadata(
pluginID: pluginID,
chunkKind: chunkKind,
chunkVersion: chunkVersion
),
payload: payload
)
}
}
21 changes: 19 additions & 2 deletions Sources/UntoldEngine/AssetFormat/UntoldReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public final class UntoldReader: @unchecked Sendable {
from: data,
entries: chunks
)
let pluginChunks = try decodePluginChunks(from: data, entries: chunks)

let decoded = UntoldDecodedAsset(
header: header,
Expand All @@ -143,7 +144,8 @@ public final class UntoldReader: @unchecked Sendable {
animationClips: animationClips,
animationChannels: animationChannels,
translationKeyframes: translationKeyframes,
rotationKeyframes: rotationKeyframes
rotationKeyframes: rotationKeyframes,
pluginChunks: pluginChunks
)
try validateDecodedAsset(decoded)
return decoded
Expand Down Expand Up @@ -396,6 +398,18 @@ public final class UntoldReader: @unchecked Sendable {
return try decodeTableIfPresent(T.self, chunkType: chunkType, from: fileData, entries: entries).first
}

private func decodePluginChunks(
from fileData: Data,
entries: [UntoldChunkEntryV1]
) throws -> [UntoldPluginChunk] {
try entries
.filter(\.chunkType.isPluginExtensionChunk)
.map { entry in
let data = try decompressChunk(entry, fileData: fileData)
return try UntoldPluginChunkEnvelope.decode(chunkType: entry.chunkType, data: data)
}
}

/// Returns decompressed chunk payload for the given chunk type.
/// Call this from external loaders (e.g. NativeFormatLoader) to retrieve
/// vertex or index data with transparent decompression.
Expand Down Expand Up @@ -489,6 +503,7 @@ public struct UntoldDecodedAsset: Sendable {
public let animationChannels: [UntoldAnimationChannelRecordV1]
public let translationKeyframes: [UntoldTranslationKeyframeRecordV1]
public let rotationKeyframes: [UntoldRotationKeyframeRecordV1]
public let pluginChunks: [UntoldPluginChunk]

public init(
header: UntoldFileHeaderV1,
Expand All @@ -508,7 +523,8 @@ public struct UntoldDecodedAsset: Sendable {
animationClips: [UntoldAnimationClipRecordV1],
animationChannels: [UntoldAnimationChannelRecordV1],
translationKeyframes: [UntoldTranslationKeyframeRecordV1],
rotationKeyframes: [UntoldRotationKeyframeRecordV1]
rotationKeyframes: [UntoldRotationKeyframeRecordV1],
pluginChunks: [UntoldPluginChunk] = []
) {
self.header = header
self.chunks = chunks
Expand All @@ -528,6 +544,7 @@ public struct UntoldDecodedAsset: Sendable {
self.animationChannels = animationChannels
self.translationKeyframes = translationKeyframes
self.rotationKeyframes = rotationKeyframes
self.pluginChunks = pluginChunks
}

public func string(at offset: UInt32) throws -> String? {
Expand Down
2 changes: 2 additions & 0 deletions Sources/UntoldEngine/AssetFormat/UntoldValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ public enum UntoldValidationError: Error, Sendable, Equatable {
actualWidth: UInt32,
actualHeight: UInt32
)
case invalidPluginChunkHeader
case unsupportedPluginChunkVersion(UInt32)
}
81 changes: 65 additions & 16 deletions Sources/UntoldEngine/ECS/ComponentPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ struct TypeInfo {
let type: Any.Type
}

public struct ComponentTypeRegistration: Equatable, Sendable {
public let id: Int
public let typeName: String

public init(id: Int, typeName: String) {
self.id = id
self.typeName = typeName
}
}

private final class ComponentIDState: @unchecked Sendable {
let lock = NSLock()
var componentIDs: [ObjectIdentifier: TypeInfo] = [:]
Expand All @@ -39,6 +49,20 @@ func componentTypeInfo(for typeId: ObjectIdentifier) -> TypeInfo? {
return typeInfo
}

public func registeredComponentTypes() -> [ComponentTypeRegistration] {
componentTypeInfosSnapshot().values.map { typeInfo in
ComponentTypeRegistration(
id: typeInfo.id,
typeName: String(describing: typeInfo.type)
)
}.sorted { lhs, rhs in
if lhs.id != rhs.id {
return lhs.id < rhs.id
}
return lhs.typeName < rhs.typeName
}
}

@inline(__always)
private func enforceECSMainActor() {
// ECS synchronization is lock-based (scene/global stores + component locks),
Expand All @@ -53,6 +77,10 @@ public func getComponentId(for type: (some Any).Type) -> Int {
if let typeInfo = componentTypeInfo(for: typeId) {
return typeInfo.id
} else {
precondition(
componentCounter < MAX_COMPONENTS,
"Exceeded maximum ECS component types (\(MAX_COMPONENTS))."
)
let id = componentCounter
componentCounter += 1

Expand Down Expand Up @@ -99,48 +127,69 @@ public struct ComponentPool {
}

public struct ComponentMask: Equatable, Hashable {
@usableFromInline var bits: UInt64 = 0
@usableFromInline var lowerBits: UInt64 = 0
@usableFromInline var upperBits: UInt64 = 0

@inlinable init() {}

@inlinable public mutating func set(_ index: Int) {
precondition(index >= 0 && index < 64)
bits |= (1 &<< index)
precondition(index >= 0 && index < 128)
if index < 64 {
lowerBits |= (1 &<< index)
} else {
upperBits |= (1 &<< (index - 64))
}
}

@inlinable public mutating func reset(_ index: Int) {
precondition(index >= 0 && index < 64)
bits &= ~(1 &<< index)
precondition(index >= 0 && index < 128)
if index < 64 {
lowerBits &= ~(1 &<< index)
} else {
upperBits &= ~(1 &<< (index - 64))
}
}

@inlinable public mutating func resetAll() {
bits = 0
lowerBits = 0
upperBits = 0
}

@inlinable public func test(_ index: Int) -> Bool {
precondition(index >= 0 && index < 64)
return (bits & (1 &<< index)) != 0
precondition(index >= 0 && index < 128)
if index < 64 {
return (lowerBits & (1 &<< index)) != 0
}
return (upperBits & (1 &<< (index - 64))) != 0
}

/// self includes all bits in `other`
@inlinable func contains(_ other: ComponentMask) -> Bool {
(bits & other.bits) == other.bits
(lowerBits & other.lowerBits) == other.lowerBits
&& (upperBits & other.upperBits) == other.upperBits
}

@inlinable func intersects(_ other: ComponentMask) -> Bool {
(bits & other.bits) != 0
(lowerBits & other.lowerBits) != 0
|| (upperBits & other.upperBits) != 0
}

@inlinable func isDisjoint(with other: ComponentMask) -> Bool {
(bits & other.bits) == 0
(lowerBits & other.lowerBits) == 0
&& (upperBits & other.upperBits) == 0
}

@inlinable func activeComponentIds() -> [Int] {
var result: [Int] = []
var b = bits
while b != 0 {
result.append(b.trailingZeroBitCount)
b &= b &- 1
var lower = lowerBits
while lower != 0 {
result.append(lower.trailingZeroBitCount)
lower &= lower &- 1
}
var upper = upperBits
while upper != 0 {
result.append(64 + upper.trailingZeroBitCount)
upper &= upper &- 1
}
return result
}
Expand All @@ -150,7 +199,7 @@ public struct ComponentMask: Equatable, Hashable {
func makeMask(from componentTypes: some Sequence<Int>) -> ComponentMask {
var m = ComponentMask()
for c in componentTypes {
if c >= 0, c < 64 { m.set(c) }
if c >= 0, c < 128 { m.set(c) }
}
return m
}
Loading
Loading