Skip to content

Commit fd635a3

Browse files
committed
[Release] Preparing release 0.12.1
1 parent 71a8dce commit fd635a3

File tree

9 files changed

+66
-71
lines changed

9 files changed

+66
-71
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
# Changelog
2+
## v0.12.1 - 2026-04-09
3+
### 🐞 Fixes
4+
- [Patch] Fixed exporter UV issues (4e5da7f…)
5+
- [Patch] Fixed texture encoding in exporter (5313ee7…)
6+
- [Patch] Improved the exporting speed. (7f14373…)
7+
- [Patch] Fixed grayscale textures rendering red in engine (a0c0f00…)
8+
- [Patch] Fixed 16-bit sRGB textures saving with wrong view transform (fe33c36…)
9+
- [Patch] Ignore exr textures during export (e9981ac…)
10+
- [Patch] Implemented LZ4 compression (7095b8d…)
11+
- [Patch] Added ASTC texture pipeline to engine. (6cd4dda…)
12+
### 📚 Docs
13+
- [Docs] Updated logo image (d4a5faf…)
14+
- [Docs] Added LZ4 documentation (bf231ab…)
215
## v0.12.0 - 2026-04-06
316
### 🐞 Fixes
417
- [Patch] improved the tile-streaming system (c09bb2c…)

README.md

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -65,43 +65,23 @@ http://www.haroldserrano.com
6565

6666
The fastest way to experience Untold Engine is to run the demo project.
6767

68-
Clone the repository, run the engine and load a USDZ file:
68+
Clone the repository and launch the demo:
6969

7070
```bash
7171
git clone https://github.com/untoldengine/UntoldEngine.git
7272
cd UntoldEngine
7373
swift run untolddemo
7474
```
7575

76-
Legacy alias (prints deprecation warning):
76+
The demo UI lets you see the engine in action right away. Using the `Remote Scene` drop-down menu, you can choose a scene to stream directly into the demo through the engine's **Asset Remote Streaming** support.
7777

78-
```bash
79-
swift run DemoGame
80-
```
81-
82-
This will:
83-
84-
- Build the engine using **Swift Package Manager**
85-
- Compile the demo project
86-
- Launch the demo so you can see the engine running immediately
87-
88-
No additional setup is required.
89-
90-
---
91-
92-
## 🧪 Sandbox
93-
94-
The sandbox is the quickest place to get a feel for Untold Engine without the demo HUD and demo-specific wiring.
78+
## I want to try my own USDZ
9579

96-
Use it when you want to try engine APIs directly, such as `setEntityMeshAsync(...)`, `loadTiledScene(...)`, or other one-off rendering and scene-loading experiments.
80+
Untold Engine uses its own native asset format: `.untold`.
9781

98-
Run it with:
99-
100-
```bash
101-
swift run sandbox
102-
```
82+
To try your own `USDZ` file, first convert it to `.untold` using the `Tools` section in the demo UI.
10383

104-
The sandbox target lives in `Sources/Sandbox` and includes a minimal `AppDelegate`, `main.swift`, and `GameScene.swift`. The intended workflow is to edit `GameScene.swift`, add the quick test you want to run, and relaunch the app.
84+
After the export is complete, open the Local Scene `Browse` drop-down menu, choose `.untold`, then browse for and select your exported `.untold` file.
10585

10686
---
10787

@@ -185,6 +165,7 @@ Untold Engine aims to support applications such as:
185165
- [Streaming Cache Lifecycle](docs/Architecture/streamingCacheLifecycle.md)
186166
- [Texture Streaming System](docs/Architecture/textureStreamingSystem.md)
187167
- [Out of Core](docs/Architecture/outOfCore.md)
168+
- [Asset Remote Streaming](docs/Architecture/asset_remote_streaming.md)
188169

189170
---
190171

Sources/DemoGame/DemoHUD.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
Text("Local Scene")
115115
.foregroundStyle(.secondary)
116116
.frame(width: 92, alignment: .leading)
117-
Menu("Select") {
117+
Menu("Browse") {
118118
Button("Asset (.untold)", action: openLocalAssetPicker)
119119
Button("Tiled Scene (.json)", action: openLocalTiledScenePicker)
120120
}

Sources/UntoldEngine/AssetFormat/NativeTexFormat.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,19 +166,19 @@ public enum NativeTexValidationError: Error, Sendable, Equatable {
166166

167167
extension NativeTexHeader: UntoldBinaryEncodable, UntoldBinaryDecodable {
168168
public func encode(to writer: UntoldBinaryWriter) {
169-
writer.writeBytes(magic) // offset 0 – 7 (8 bytes)
170-
writer.writeUInt32LE(version) // offset 8 – 11 (4 bytes)
171-
writer.writeUInt32LE(flags) // offset 12 – 15 (4 bytes)
172-
writer.writeUInt32LE(width) // offset 16 – 19 (4 bytes)
173-
writer.writeUInt32LE(height) // offset 20 – 23 (4 bytes)
174-
writer.writeUInt32LE(mipCount) // offset 24 – 27 (4 bytes)
175-
writer.writeUInt32LE(pixelFormat) // offset 28 – 31 (4 bytes)
176-
writer.writeUInt8(blockWidth) // offset 32 (1 byte)
177-
writer.writeUInt8(blockHeight) // offset 33 (1 byte)
178-
writer.writeBytes(reserved) // offset 34 – 35 (2 bytes)
179-
writer.writeUInt32LE(payloadOffset) // offset 36 – 39 (4 bytes)
169+
writer.writeBytes(magic) // offset 0 – 7 (8 bytes)
170+
writer.writeUInt32LE(version) // offset 8 – 11 (4 bytes)
171+
writer.writeUInt32LE(flags) // offset 12 – 15 (4 bytes)
172+
writer.writeUInt32LE(width) // offset 16 – 19 (4 bytes)
173+
writer.writeUInt32LE(height) // offset 20 – 23 (4 bytes)
174+
writer.writeUInt32LE(mipCount) // offset 24 – 27 (4 bytes)
175+
writer.writeUInt32LE(pixelFormat) // offset 28 – 31 (4 bytes)
176+
writer.writeUInt8(blockWidth) // offset 32 (1 byte)
177+
writer.writeUInt8(blockHeight) // offset 33 (1 byte)
178+
writer.writeBytes(reserved) // offset 34 – 35 (2 bytes)
179+
writer.writeUInt32LE(payloadOffset) // offset 36 – 39 (4 bytes)
180180
writer.writeUInt32LE(totalPayloadSize) // offset 40 – 43 (4 bytes)
181-
for word in reserved1 { // offset 44 – 63 (5 × 4 = 20 bytes)
181+
for word in reserved1 { // offset 44 – 63 (5 × 4 = 20 bytes)
182182
writer.writeUInt32LE(word)
183183
}
184184
// Total: 64 bytes

Sources/UntoldEngine/RuntimeAssets/NativeTextureLoader.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public final class NativeTextureLoader: @unchecked Sendable {
116116
)
117117
desc.mipmapLevelCount = mipCount
118118
desc.storageMode = .private
119-
desc.usage = .shaderRead // ASTC blocks cannot be reinterpreted; no .pixelFormatView needed
119+
desc.usage = .shaderRead // ASTC blocks cannot be reinterpreted; no .pixelFormatView needed
120120

121121
guard let texture = device.makeTexture(descriptor: desc) else {
122122
throw NativeTexLoadError.textureAllocationFailed
@@ -148,7 +148,7 @@ public final class NativeTextureLoader: @unchecked Sendable {
148148
let mipH = Int(mip.heightPx)
149149
let blocksWide = (mipW + blockW - 1) / blockW
150150
let blocksHigh = (mipH + blockH - 1) / blockH
151-
let bytesPerRow = blocksWide * 16 // each ASTC block is always 16 bytes
151+
let bytesPerRow = blocksWide * 16 // each ASTC block is always 16 bytes
152152
let bytesPerImage = bytesPerRow * blocksHigh
153153

154154
// Slice the mip payload out of the memory-mapped file data.
@@ -191,7 +191,7 @@ public final class NativeTextureLoader: @unchecked Sendable {
191191

192192
blitEncoder.endEncoding()
193193
commandBuffer.commit()
194-
commandBuffer.waitUntilCompleted() // synchronous for Phase 2; async upload in a later phase
194+
commandBuffer.waitUntilCompleted() // synchronous for Phase 2; async upload in a later phase
195195

196196
if let error = commandBuffer.error {
197197
throw NativeTexLoadError.gpuUploadFailed(error)

Sources/UntoldEngine/Systems/TextureStreamingSystem.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -672,9 +672,9 @@ public class TextureStreamingSystem: @unchecked Sendable {
672672
// Both figures are multiplied by 4/3 to account for the full mip chain.
673673
let bytesPerPixel: Int
674674
if case .utex = item.slot.source {
675-
bytesPerPixel = 1 // ASTC: ≤ 1 byte/pixel for all block sizes
675+
bytesPerPixel = 1 // ASTC: ≤ 1 byte/pixel for all block sizes
676676
} else {
677-
bytesPerPixel = 4 // RGBA8
677+
bytesPerPixel = 4 // RGBA8
678678
}
679679
let newBytes = newDim * newDim * bytesPerPixel * 4 / 3
680680
let oldBytes = oldDim * oldDim * bytesPerPixel * 4 / 3
@@ -788,7 +788,7 @@ public class TextureStreamingSystem: @unchecked Sendable {
788788
// ASTC .utex textures: select the starting mip by dimension instead of
789789
// MPS resampling (compressed ASTC blocks cannot be resampled in-place).
790790
// Both upgrade and downgrade reload from the file at the target tier.
791-
if case .utex(let url) = item.slot.source {
791+
if case let .utex(url) = item.slot.source {
792792
texture = capturedNativeLoader?.loadTexture(
793793
from: url,
794794
targetMaxDimension: item.targetMaxDimension,

Tests/UntoldEngineTests/NativeFormatTests.swift

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,6 @@ extension NativeFormatTests {
544544
// MARK: - LZ4 compression tests
545545

546546
extension NativeFormatTests {
547-
548547
// MARK: Helpers
549548

550549
/// Compress `input` with COMPRESSION_LZ4_RAW — the same algorithm the runtime uses to decompress.
@@ -598,10 +597,10 @@ extension NativeFormatTests {
598597
let originalIndexData = indexWriter.data
599598

600599
let compressedVertex = lz4Compress(originalVertexData)
601-
let compressedIndex = lz4Compress(originalIndexData)
600+
let compressedIndex = lz4Compress(originalIndexData)
602601

603602
let vertexUncompressedSize: UInt64 = inflateVertexUncompressedSize
604-
? UInt64(originalVertexData.count) + 1000 // deliberately wrong
603+
? UInt64(originalVertexData.count) + 1000 // deliberately wrong
605604
: UInt64(originalVertexData.count)
606605

607606
let entity = UntoldEntityRecordV1(
@@ -653,21 +652,21 @@ extension NativeFormatTests {
653652

654653
// (storedBytes, compressionType, uncompressedSize, elementCount)
655654
let specs: [(UntoldChunkType, Data, UntoldCompressionType, UInt64, UInt32)] = [
656-
(.stringTable, stringTable.data, .none, UInt64(stringTable.data.count), 0),
657-
(.entityTable, encodeChunk([entity]), .none, UInt64(encodeChunk([entity]).count), 1),
658-
(.meshTable, encodeChunk([mesh]), .none, UInt64(encodeChunk([mesh]).count), 1),
659-
(.materialTable, encodeChunk([material]),.none, UInt64(encodeChunk([material]).count), 1),
660-
(.textureTable, encodeChunk([texture]), .none, UInt64(encodeChunk([texture]).count), 1),
661-
(.vertexData, compressedVertex, .lz4, vertexUncompressedSize, 0),
662-
(.indexData, compressedIndex, .lz4, UInt64(originalIndexData.count), 0),
655+
(.stringTable, stringTable.data, .none, UInt64(stringTable.data.count), 0),
656+
(.entityTable, encodeChunk([entity]), .none, UInt64(encodeChunk([entity]).count), 1),
657+
(.meshTable, encodeChunk([mesh]), .none, UInt64(encodeChunk([mesh]).count), 1),
658+
(.materialTable, encodeChunk([material]), .none, UInt64(encodeChunk([material]).count), 1),
659+
(.textureTable, encodeChunk([texture]), .none, UInt64(encodeChunk([texture]).count), 1),
660+
(.vertexData, compressedVertex, .lz4, vertexUncompressedSize, 0),
661+
(.indexData, compressedIndex, .lz4, UInt64(originalIndexData.count), 0),
663662
]
664663

665664
header.chunkCount = UInt32(specs.count)
666665

667666
// Compute chunk table size (matches UntoldChunkEntryV1 binary layout: 2×u32 + 3×u64 + 2×u32 = 40 bytes)
668667
let chunkEntrySize = MemoryLayout<UInt32>.size * 2
669-
+ MemoryLayout<UInt64>.size * 3
670-
+ MemoryLayout<UInt32>.size * 2
668+
+ MemoryLayout<UInt64>.size * 3
669+
+ MemoryLayout<UInt32>.size * 2
671670
let headerWriter2 = UntoldBinaryWriter()
672671
header.encode(to: headerWriter2)
673672
let headerSize = headerWriter2.data.count
@@ -708,26 +707,26 @@ extension NativeFormatTests {
708707
let decoded = try reader.readAsset(from: fileData)
709708

710709
let decompressedVertex = try reader.readChunkData(.vertexData, from: fileData, entries: decoded.chunks)
711-
let decompressedIndex = try reader.readChunkData(.indexData, from: fileData, entries: decoded.chunks)
710+
let decompressedIndex = try reader.readChunkData(.indexData, from: fileData, entries: decoded.chunks)
712711

713712
XCTAssertEqual(decompressedVertex, originalVertexData, "Decompressed vertex data must match original")
714-
XCTAssertEqual(decompressedIndex, originalIndexData, "Decompressed index data must match original")
713+
XCTAssertEqual(decompressedIndex, originalIndexData, "Decompressed index data must match original")
715714
}
716715

717716
func testLZ4CompressedChunkReportsCorrectSizeMetadata() throws {
718717
let (fileData, originalVertexData, originalIndexData) = makeLZ4CompressedFile()
719718
let decoded = try UntoldReader().readAsset(from: fileData)
720719

721720
let vertexEntry = try XCTUnwrap(decoded.chunks.first { $0.chunkType == .vertexData })
722-
let indexEntry = try XCTUnwrap(decoded.chunks.first { $0.chunkType == .indexData })
721+
let indexEntry = try XCTUnwrap(decoded.chunks.first { $0.chunkType == .indexData })
723722

724-
XCTAssertEqual(vertexEntry.compressionType, .lz4)
725-
XCTAssertEqual(vertexEntry.uncompressedSize, UInt64(originalVertexData.count))
723+
XCTAssertEqual(vertexEntry.compressionType, .lz4)
724+
XCTAssertEqual(vertexEntry.uncompressedSize, UInt64(originalVertexData.count))
726725
XCTAssertLessThanOrEqual(vertexEntry.compressedSize, vertexEntry.uncompressedSize + 64,
727-
"LZ4 compressed size should not wildly exceed original for small inputs")
726+
"LZ4 compressed size should not wildly exceed original for small inputs")
728727

729-
XCTAssertEqual(indexEntry.compressionType, .lz4)
730-
XCTAssertEqual(indexEntry.uncompressedSize, UInt64(originalIndexData.count))
728+
XCTAssertEqual(indexEntry.compressionType, .lz4)
729+
XCTAssertEqual(indexEntry.uncompressedSize, UInt64(originalIndexData.count))
731730
}
732731

733732
func testLZ4CompressionOutputSizeMismatchIsRejected() throws {

Tests/UntoldEngineTests/NativeTexFormatTests.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ final class NativeTexFormatTests: XCTestCase {
9191
// MARK: - Mip entry roundtrip
9292

9393
func testMipEntryRoundtrip() throws {
94-
let original = NativeTexMipEntry(byteOffset: 256, byteSize: 16_384, widthPx: 1024, heightPx: 512)
94+
let original = NativeTexMipEntry(byteOffset: 256, byteSize: 16384, widthPx: 1024, heightPx: 512)
9595
let decoded = try NativeTexMipEntry.decode(from: UntoldBinaryReader(data: encode(original)))
9696
XCTAssertEqual(decoded, original)
9797
}
@@ -114,7 +114,9 @@ final class NativeTexFormatTests: XCTestCase {
114114

115115
let writer = UntoldBinaryWriter()
116116
header.encode(to: writer)
117-
for mip in mips { mip.encode(to: writer) }
117+
for mip in mips {
118+
mip.encode(to: writer)
119+
}
118120
let data = writer.data
119121

120122
let (decodedHeader, decodedMips) = try NativeTexReader().read(from: data)
@@ -131,7 +133,7 @@ final class NativeTexFormatTests: XCTestCase {
131133

132134
func testReaderRejectsInvalidMagic() throws {
133135
var data = makeMinimalContainer(mipCount: 1)
134-
data[0] = 0xFF // corrupt first byte of magic
136+
data[0] = 0xFF // corrupt first byte of magic
135137

136138
XCTAssertThrowsError(try NativeTexReader().read(from: data)) { error in
137139
XCTAssertEqual(error as? NativeTexValidationError, .invalidMagic)
@@ -178,7 +180,7 @@ final class NativeTexFormatTests: XCTestCase {
178180
var header = makeHeader()
179181
header.mipCount = 1
180182
header.payloadOffset = payloadOffset
181-
header.totalPayloadSize = 100 // deliberately small
183+
header.totalPayloadSize = 100 // deliberately small
182184

183185
let overflowMip = NativeTexMipEntry(byteOffset: 0, byteSize: 9999, widthPx: 64, heightPx: 64)
184186

@@ -218,7 +220,7 @@ final class NativeTexFormatTests: XCTestCase {
218220
width: 1024,
219221
height: 512,
220222
mipCount: 10,
221-
pixelFormat: 186, // MTLPixelFormat.astc_4x4_sRGB
223+
pixelFormat: 186, // MTLPixelFormat.astc_4x4_sRGB
222224
blockWidth: 4,
223225
blockHeight: 4,
224226
payloadOffset: NativeTexFormat.payloadOffset(mipCount: 10),
@@ -242,7 +244,7 @@ final class NativeTexFormatTests: XCTestCase {
242244
/// so NativeTexReader can parse it without payload-bounds failures.
243245
private func makeMinimalContainer(mipCount: Int) -> Data {
244246
var header = makeHeader()
245-
let mipByteSize: UInt32 = 16 // one ASTC block per mip (smallest valid payload)
247+
let mipByteSize: UInt32 = 16 // one ASTC block per mip (smallest valid payload)
246248
let payloadOffset = NativeTexFormat.payloadOffset(mipCount: mipCount)
247249
header.mipCount = UInt32(mipCount)
248250
header.payloadOffset = payloadOffset

docs/images/engine-highlight-1.png

500 KB
Loading

0 commit comments

Comments
 (0)