diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json new file mode 100644 index 000000000..77e9744d2 --- /dev/null +++ b/.cursor/worktrees.json @@ -0,0 +1,5 @@ +{ + "setup-worktree": [ + "npm install" + ] +} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c4c18f1e2..18f90be77 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,12 +15,12 @@ permissions: jobs: build-and-deploy-docs: - runs-on: macos-15 + runs-on: macos-26 steps: - name: Setup Xcode version uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.2.0' + xcode-version: '26.1.1' - uses: actions/checkout@v4 - name: Build Documentation run: swift package --allow-writing-to-directory ./docs generate-documentation --target AdaEngine --output-path ./docs --transform-for-static-hosting --hosting-base-path adaengine-docs diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 7409e1db3..9c0ae083d 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -9,11 +9,11 @@ on: jobs: MacOS: - name: macOS 15 with Xcode ${{ matrix.xcode_version }} + name: macOS 26 with Xcode ${{ matrix.xcode_version }} strategy: matrix: - xcode_version: ['16.2', '16.3'] - runs-on: macos-15 + xcode_version: ['26.1.1'] + runs-on: macos-26 env: DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode_version }}.app steps: diff --git a/.swiftlint.yml b/.swiftlint.yml index 2d55f3598..65e79dd4f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,4 +1,64 @@ -# By default, SwiftLint uses a set of sensible default rules you can adjust: +analyzer_rules: + - capture_variable + - unused_declaration + - unused_import + +opt_in_rules: + - array_init + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_first_not_nil + - discouraged_assert + - empty_collection_literal + - empty_count + - empty_string + - enum_case_associated_values_count + - expiring_todo + - explicit_init + - fatal_error_message + - file_name_no_space + - first_where + - force_unwrapping + - identical_operands + - last_where + - legacy_objc_type + - literal_expression_end_indentation + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_in_static_references + - prefer_zero_over_explicit_init + - private_subject + - prohibited_interface_builder + - prohibited_super_call + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - single_test_class + - sorted_first_last + - sorted_imports + - strict_fileprivate + - type_contents_order + - unavailable_function + - unneeded_parentheses_in_closure_argument + - untyped_error_in_catch + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - weak_delegate + - yoda_condition + disabled_rules: # rule identifiers turned on by default to exclude from running - colon - comma @@ -9,29 +69,18 @@ disabled_rules: # rule identifiers turned on by default to exclude from running - large_tuple - force_cast - function_parameter_count - - shorthand_operator - identifier_name + - shorthand_operator - todo -opt_in_rules: # some rules are turned off by default, so you need to opt-in - - empty_count # Find all the available rules by running: `swiftlint rules` - - private_over_fileprivate - -# Alternatively, specify all rules explicitly by uncommenting this option: -# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this -# - empty_parameters -# - vertical_whitespace included: # paths to include during linting. `--path` is ignored if present. - - Sources/AdaEngine - - Sources/AdaEditor - - Sources/Math + - Sources + excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage - Pods - Plugins - Sources/AdaEngine/AdaEngine.docc/ -analyzer_rules: # Rules run by `swiftlint analyze` (experimental) - - explicit_self # configurable rules can be customized from this configuration file # binary rules can set their severity level diff --git a/Assets/Examples/2d/BunniesStress/BunniesStressApp.swift b/Assets/Examples/2d/BunniesStress/BunniesStressApp.swift new file mode 100644 index 000000000..4bec3ab15 --- /dev/null +++ b/Assets/Examples/2d/BunniesStress/BunniesStressApp.swift @@ -0,0 +1,332 @@ +// +// BunniesStressApp.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 07.12.2025. +// + +import AdaEngine + +@main +struct AdaEditorApp: App { + var body: some AppScene { + EmptyWindow() + .addPlugins( + DefaultPlugins(), + BunnyExample() + ) + .windowMode(.windowed) + .windowTitle("AdaEngine") + } +} + +enum BunnyExampleConstants { + static let bunniesPerClick: Int = 10 + static let bunnyScale: Float = 0.1 + static let gravity: Float = -9.8 + static let maxVelocity: Float = 3050.0 +} + +/// A bunny stress test scene similar to bevymark. +/// Click to spawn bunnies that bounce around the screen with gravity simulation. +@MainActor +struct BunnyExample: Plugin { + func setup(in app: AppWorlds) { + setupCamera(in: app) + loadAssets(in: app) + setupUI(in: app) + setupSystems(in: app) + } + + private func setupCamera(in app: AppWorlds) { + app.main.spawn( + "Camera", + bundle: OrthographicCameraBundle( + camera: Camera() + .setBackgroundColor(.gray) + ) + ) + } + + private func loadAssets(in app: AppWorlds) { + // Try to load a bunny texture, fallback to white texture if not available + do { + let image = try AssetsManager.loadSync( + Image.self, + at: "Resources/characters_packed.png", + from: Bundle.module + ).asset! + let atlas = TextureAtlas(from: image, size: [20, 23], margin: [4, 1]) + app.insertResource(BunnyTexture(texture: AssetHandle(atlas[0, 0]))) + } catch { + print("Could not load bunny texture, using white texture: \(error)") + app.insertResource(BunnyTexture(texture: AssetHandle(Texture2D.whiteTexture))) + } + } + + private func setupUI(in app: AppWorlds) { + // Create performance counter UI + var container = TextAttributeContainer() + container.foregroundColor = .white + + app.main.spawn("PerformanceCounter") { + TextComponent(text: AttributedText("Bunnies: 0\nFPS: 0", attributes: container)) + Transform(scale: Vector3(0.1), position: [-9, 8, 1]) + NoFrustumCulling() + PerformanceCounter() + } + } + + private func setupSystems(in app: AppWorlds) { + app + .addSystem(BunnySpawnerSystem.self) + .addSystem(BunnyMovementSystem.self) + .addSystem(BunnyCollisionSystem.self) + .addSystem(PerformanceCounterSystem.self) + } +} + +// MARK: - Components + +struct BunnyTexture: Resource { + let texture: AssetHandle +} + +/// Component to mark bunny entities and store their velocity +@Component +struct Bunny { + private static let maxInitialVelocity: Float = 9000.0 + + var velocity: Vector3 + + init() { + // Initialize with random velocity + let velocityX = Float.random(in: -Self.maxInitialVelocity...Self.maxInitialVelocity) + let velocityY = Float.random(in: 0...Self.maxInitialVelocity) + self.velocity = Vector3(velocityX, velocityY, 0) + } +} + +/// Component for the performance counter UI +@Component +struct PerformanceCounter { + var bunnyCount: Int = 0 + var fps: Float = 0 + var frameCount: Int = 0 + var lastUpdateTime: TimeInterval = 0 +} + +// MARK: - Systems + +/// System that spawns bunnies on mouse click +@PlainSystem +struct BunnySpawnerSystem { + @Query + private var cameras + + @Res + private var bunnyTexture: BunnyTexture! + + @Res + private var input: Input! + + @Commands + private var commands + + init(world: World) {} + + func update(context: UpdateContext) { + guard input.isMouseButtonPressed(.left) else { + return + } + + // Get camera for world position conversion + cameras.forEach { camera, globalTransform in + let mousePosition = input.getMousePosition() + guard let worldPosition = camera.viewportToWorld2D( + cameraGlobalTransform: globalTransform.matrix, + viewportPosition: mousePosition + ) else { return } + + // Spawn multiple bunnies at mouse position + for _ in 0 ..< BunnyExampleConstants.bunniesPerClick { + spawnBunny(at: Vector3(worldPosition.x, -worldPosition.y, 0), world: context.world) + } + } + } + + private func spawnBunny(at position: Vector3, world: World) { + // Add small random offset to position + let offsetX = Float.random(in: -2.5...2.5) + let offsetY = Float.random(in: -2.5...2.5) + let bunnyPosition = position + Vector3(offsetX, offsetY, 0) + + commands.spawn("Bunny") { + Bunny() + Transform( + scale: Vector3(BunnyExampleConstants.bunnyScale), + position: bunnyPosition + ) + SpriteComponent( + texture: bunnyTexture.texture, + tintColor: getRandomColor() + ) + NoFrustumCulling() + } + } + + private func getRandomColor() -> Color { + return Color( + red: Float.random(in: 0.3...1.0), + green: Float.random(in: 0.3...1.0), + blue: Float.random(in: 0.3...1.0), + alpha: 1 + ) + } +} + +/// System that handles bunny movement with gravity +@PlainSystem +struct BunnyMovementSystem { + @Query, Ref> + private var bunnies + + @Res + private var deltaTime + + init(world: World) {} + + func update(context: UpdateContext) async { + let deltaTime = deltaTime.deltaTime + + await bunnies.parallel().forEach { bunny, transform in + var velocity = bunny.velocity + var position = transform.position + + // Apply gravity + velocity.y += BunnyExampleConstants.gravity * deltaTime + + // Clamp velocity to maximum + if velocity.length > BunnyExampleConstants.maxVelocity { + velocity = velocity.normalized * BunnyExampleConstants.maxVelocity + } + + // Update position + position += velocity * deltaTime + + // Update components + bunny.velocity = velocity + transform.wrappedValue = Transform( + rotation: transform.rotation, + scale: transform.scale, + position: position + ) + } + } +} + +/// System that handles collision with screen boundaries +@PlainSystem +struct BunnyCollisionSystem { + @FilterQuery> + private var cameras + + @Query, Ref> + private var bunnies + + init(world: World) {} + + func update(context: UpdateContext) async { + // Get screen bounds from camera + guard let camera = cameras.first else { + return + } + + let viewport = camera.viewport?.rect ?? Rect(x: 0, y: 0, width: 800, height: 600) + let halfExtents = Vector2(Float(viewport.width / 8), Float(viewport.height / 8)) + + // Convert to world coordinates (simplified approach) + let worldHalfExtents = halfExtents * camera.orthographicScale / 100.0 + + await bunnies.parallel().forEach { bunny, transform in + var velocity = bunny.velocity + var position = transform.position + let halfBunnySize = BunnyExampleConstants.bunnyScale * 0.5 + + // Check horizontal bounds + if (velocity.x > 0 && position.x + halfBunnySize > worldHalfExtents.x) || + (velocity.x <= 0 && position.x - halfBunnySize < -worldHalfExtents.x) { + velocity.x = -velocity.x + } + + // Check vertical bounds + if velocity.y < 0 && position.y - halfBunnySize < -worldHalfExtents.y { + velocity.y = -velocity.y + } + + // Check top bound (stop upward velocity) + if position.y + halfBunnySize > worldHalfExtents.y && velocity.y > 0 { + velocity.y = 0 + } + + // Keep bunny in bounds + position.x = max( + -worldHalfExtents.x + halfBunnySize, + min(worldHalfExtents.x - halfBunnySize, position.x) + ) + position.y = max( + -worldHalfExtents.y + halfBunnySize, + min(worldHalfExtents.y - halfBunnySize, position.y) + ) + + // Update components + bunny.velocity = velocity + transform.wrappedValue = Transform( + rotation: transform.rotation, + scale: transform.scale, + position: position + ) + } + } +} + +/// System that updates the performance counter +@PlainSystem +struct PerformanceCounterSystem { + @Query + private var bunnies + + @Query, Ref> + private var counters + + @Res + private var deltaTime + + init(world: World) {} + + func update(context: UpdateContext) { + let bunnyCount = bunnies.count + let deltaTime = deltaTime.deltaTime + + counters.forEach { _, counter, textComponent in + counter.bunnyCount = bunnyCount + counter.frameCount += 1 + counter.lastUpdateTime += deltaTime + + // Update FPS calculation every second + if counter.lastUpdateTime >= 1.0 { + counter.fps = Float(counter.frameCount) / Float(counter.lastUpdateTime) + counter.frameCount = 0 + counter.lastUpdateTime = 0 + } + + // Update text + var container = TextAttributeContainer() + container.foregroundColor = .white + + let text = unsafe "Bunnies: \(bunnyCount)\nFPS: \(String(format: "%.1f", counter.fps))" + print(text) + textComponent.text = AttributedText(text, attributes: container) + } + } +} diff --git a/Assets/Examples/2d/CustomMaterial/CustomMaterialApp.swift b/Assets/Examples/2d/CustomMaterial/CustomMaterialApp.swift new file mode 100644 index 000000000..01e2fdf6b --- /dev/null +++ b/Assets/Examples/2d/CustomMaterial/CustomMaterialApp.swift @@ -0,0 +1,77 @@ +// +// CustomMaterialApp.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 09.12.2025. +// + +import AdaEngine + +@main +struct CustomMaterialApp: App { + var body: some AppScene { + EmptyWindow() + .addPlugins( + DefaultPlugins(), + CustomMaterialPlugin(), + ) + .windowMode(.windowed) + } +} + +struct CustomMaterialPlugin: Plugin { + func setup(in app: borrowing AdaApp.AppWorlds) { + let texture = try! AssetsManager.loadSync(Texture2D.self, at: "Resources/dog.png", from: .module) + app.main.spawn("Custom Material") { + Mesh2DComponent(mesh: .generate(from: Quad(size: .one)), materials: [ + CustomMaterial(MyMaterial(color: .red, customTexture: texture.asset)) + ]) + Transform() + } + app.main.spawn(bundle: OrthographicCameraBundle(camera: Camera())) + app.main.addSystem(UpdateMaterialSystem.self) + } +} + +struct MyMaterial: CanvasMaterial { + + @Uniform(binding: 2, propertyName: "u_Time") + var time: Float + + @Uniform(binding: 2, propertyName: "u_Color") + var color: Color + + @FragmentTexture(binding: 0) + var customTexture: Texture2D + + init(color: Color, customTexture: Texture2D) { + self.time = 0 + self.color = color + self.customTexture = customTexture + } + + static func fragmentShader() throws -> AssetHandle { + try AssetsManager.loadSync( + ShaderSource.self, + at: "Resources/custom_material.glsl", + from: .module + ) + } +} + +@System +func UpdateMaterial( + _ meshes: Query, + _ input: Res, + _ delta: Res +) { + meshes.forEach { ent, mesh in + if input.wrappedValue.isMouseButtonPressed(.left) { + (mesh.materials[0] as? CustomMaterial)?.color = .mint + } else { + (mesh.materials[0] as? CustomMaterial)?.color = .pink + } + + (mesh.materials[0] as? CustomMaterial)?.time += delta.deltaTime + } +} diff --git a/Assets/Examples/2d/TransformEntChildren/TransformEntChildren.swift b/Assets/Examples/2d/TransformEntChildren/TransformEntChildren.swift new file mode 100644 index 000000000..14cfd6f9a --- /dev/null +++ b/Assets/Examples/2d/TransformEntChildren/TransformEntChildren.swift @@ -0,0 +1,88 @@ +// +// TransformEntChildrenScene.swift +// +// +// Created by v.prusakov on 5/4/24. +// + +import AdaEngine + +@main +struct TransformEntChildrenApp: App { + var body: some AppScene { + EmptyWindow() + .addPlugins( + DefaultPlugins(), + TransformEntChildrenPlugin() + ) + .windowMode(.windowed) + } +} + +struct TransformEntChildrenPlugin: Plugin { + + @Local private var characterAtlas: TextureAtlas! + + func setup(in app: borrowing AppWorlds) { + let charactersTiles = try! AssetsManager.loadSync( + Image.self, + at: "Resources/characters_packed.png", + from: Bundle.module + ).asset! + self.characterAtlas = TextureAtlas(from: charactersTiles, size: [20, 23], margin: [4, 1]) + + var camera = Camera() + camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) + camera.clearFlags = .solid + camera.orthographicScale = 1.5 + app.main.spawn(bundle: OrthographicCameraBundle(camera: camera)) + + let parent = app.main.spawn("parent") { + Transform(scale: Vector3(0.5), position: [0, 0, 0]) + SpriteComponent(texture: characterAtlas[0, 0]) + ParentComponent() + + Collision2DComponent( + shapes: [.generateBox()], + mode: .trigger + ) + } + + let child = app.main.spawn("child") { + Transform(scale: Vector3(0.4), position: [0.5, -0.5, 0]) + SpriteComponent(texture: characterAtlas[0, 1]) + Collision2DComponent( + shapes: [.generateBox(width: 0.4, height: 0.4)], + mode: .trigger + ) + } + + parent.addChild(child) + + app.main.addSystem(ParentMovementSystem.self) + } +} + +@Component +struct ParentComponent {} + +@PlainSystem +struct ParentMovementSystem { + + @FilterQuery, With> + private var parents + + @Res + private var deltaTime + + @Local var time: TimeInterval = 0 + + init(world: World) { } + + func update(context: UpdateContext) async { + time += deltaTime.deltaTime + parents.forEach { transform in + transform.position.x = Float(Math.sin(time)) * 1 + } + } +} diff --git a/Assets/Examples/Games/SnowmanAttacks/SnowmanAttacks.swift b/Assets/Examples/Games/SnowmanAttacks/SnowmanAttacks.swift new file mode 100644 index 000000000..0bf2ef237 --- /dev/null +++ b/Assets/Examples/Games/SnowmanAttacks/SnowmanAttacks.swift @@ -0,0 +1,457 @@ +// +// SpaceInvaders.swift +// AdaEngine +// +// Created by v.prusakov on 4/6/23. +// + +import AdaEngine + +@main +struct SnowmanAttacksApp: App { + var body: some AppScene { + EmptyWindow() + .addPlugins( + DefaultPlugins(), + SnowmanAttacks() + ) + .windowMode(.windowed) + } +} + +@MainActor +struct SnowmanAttacks: Plugin { + @Local var disposeBag: Set = [] + @Local var characterAtlas: TextureAtlas! + + func setup(in app: borrowing AppWorlds) { + let sound = try! AssetsManager.loadSync( + AudioResource.self, + at: "Resources/WindlessSlopes.wav", + from: Bundle.module + ).asset + + let charactersTiles = try! AssetsManager.loadSync( + Image.self, + at: "Resources/characters_packed.png", + from: Bundle.module + ) + + self.characterAtlas = TextureAtlas( + from: charactersTiles.asset!, size: [20, 23], margin: [4, 1]) + + var camera = Camera() + camera.clearFlags = .solid + camera.backgroundColor = .black + + try! self.makePlayer(in: app) + try! self.makeScore(in: app) + + let entity = app.main.spawn(bundle: OrthographicCameraBundle(camera: camera)) + entity.prepareAudio(sound!) + .setLoop(true) + .setVolume(0.6) + .play() + + app.main.subscribe(to: CollisionEvents.Began.self) { event in + if let bullet = event.entityB.components[Bullet.self], + var enemy = event.entityA.components[EnemyComponent.self] + { + enemy.health -= bullet.damage + + event.entityA.components += enemy + event.entityB.removeFromWorld() + } + } + .store(in: &self.disposeBag) + + app.main.subscribe(to: SceneEvents.OnReady.self) { event in + Task { @MainActor in + event.scene.world.physicsWorld2D?.gravity = .zero + } + }.store(in: &self.disposeBag) + + app.main.addSystem(MovementSystem.self) + app.main.addSystem(FireSystem.self) + app.main.addSystem(BulletSystem.self) + app.main.addSystem(EnemySpawnerSystem.self) + app.main.addSystem(EnemyMovementSystem.self) + app.main.addSystem(EnemyLifetimeSystem.self) + app.main.addSystem(EnemyExplosionSystem.self) + + app.main.addSystem(ScoreSystem.self) + } + + private func makePlayer(in app: borrowing AppWorlds) throws { + app.main.spawn { + Transform(scale: Vector3(0.2), position: [0, -0.85, 0]) + PlayerComponent() + SpriteComponent(texture: characterAtlas[7, 1]) + } + } + + private func makeScore(in app: borrowing AppWorlds) throws { + var container = TextAttributeContainer() + container.foregroundColor = .white + let attributedText = AttributedText("Score: 0", attributes: container) + + app.main.spawn("Score") { + TextComponent(text: attributedText) + Transform(scale: Vector3(0.1), position: [-0.2, -0.9, 0]) + NoFrustumCulling() + } + + app.insertResource(GameState()) + } +} + +@Component +struct PlayerComponent {} + +@Component +struct Bullet { + var damage: Float = 30 + let lifetime: Float + var currentLifetime: Float = 0 +} + +@Component +struct EnemyComponent { + var health: Float + let lifetime: Float + var currentLifetime: Float = 0 +} + +struct GameState: Resource { + var score: Int = 0 +} + +extension CollisionGroup { + static let bullet = CollisionGroup(rawValue: 1 << 2) +} + +@Component +struct ExplosionComponent { } + +@System +func Movement( + _ cameras: Query, + _ players: FilterQuery, With>, + _ input: Res +) { + guard let (globalTransform, camera) = cameras.first else { + return + } + let mousePosition = input.wrappedValue.getMousePosition() + let worldPosition = + camera.viewportToWorld2D( + cameraGlobalTransform: globalTransform.matrix, + viewportPosition: mousePosition + ) ?? .zero + + players.forEach { transform in + transform.position.x = worldPosition.x + transform.position.y = -worldPosition.y + } +} + +@PlainSystem +struct FireSystem { + + @FilterQuery, With> + private var players + + @Local + private var fixedTime = FixedTimestep(stepsPerSecond: 12) + + let laserAudio: AudioResource + + @Res + private var deltaTime + + @Res + private var input + + @Commands + private var commands + + init(world: World) { + self.laserAudio = try! AssetsManager.loadSync( + AudioResource.self, + at: "Resources/laserShoot.wav", + from: .module + ).asset + } + + func update(context: UpdateContext) async { + await self.players.forEach { entity, transform in + if input.isMouseButtonPressed(.left) || input.isKeyPressed(.space) { + + let result = fixedTime.advance(with: deltaTime.deltaTime) + + if result.isFixedTick { + let controller = await entity.prepareAudio(self.laserAudio) + + if controller.isPlaying { + controller.stop() + } + + controller.volume = 0.15 + controller.play() + + fireBullet(shipTransform: transform.wrappedValue) + } + } + } + } + + func fireBullet(shipTransform: Transform) { + let bulletScale = Vector3(0.02, 0.04, 0.04) + + var collision = PhysicsBody2DComponent( + shapes: [ + .generateBox() + ], + mass: 1, + mode: .dynamic + ) + collision.filter.categoryBitMask = .bullet + + commands.spawn("Bullet") { [collision] in + Transform(scale: bulletScale, position: shipTransform.position) + SpriteComponent(tintColor: .red) + Bullet(lifetime: 4) + collision + } + } +} + +@PlainSystem +struct BulletSystem { + + @Query, Ref> + private var bullets + + let bulletSpeed: Float = 3 + + @Res + private var deltaTime + + @Commands + private var commands + + init(world: World) { } + + func update(context: UpdateContext) { + bullets.forEach { entity, bullet, body in + body.linearVelocity = [0, bulletSpeed] + bullet.currentLifetime += deltaTime.deltaTime + + if bullet.wrappedValue.lifetime < bullet.currentLifetime { + commands.entity(entity.id) + .removeFromWorld() + } + } + } +} + +@PlainSystem +struct EnemySpawnerSystem { + + @Local + private var fixedTime = FixedTimestep(stepsPerSecond: 2) + + let textureAtlas: TextureAtlas + + @Res + private var deltaTime + + @Commands + private var commands + + init(world: World) { + do { + let tiles = try AssetsManager.loadSync( + Image.self, + at: "Resources/tiles_packed.png", + from: Bundle.module + ).asset! + + self.textureAtlas = TextureAtlas(from: tiles, size: [18, 18]) + } catch { + fatalError(error.localizedDescription) + } + } + + func update(context: UpdateContext) async { + let result = fixedTime.advance(with: deltaTime.deltaTime) + + if result.isFixedTick { + self.spawnEnemy() + } + } + + func spawnEnemy() { + var collision = Collision2DComponent( + shapes: [ + .generateBox() + ], + mode: .trigger + ) + collision.filter.collisionBitMask = .bullet + commands.spawn("Enemy") { [collision] in + Transform( + scale: Vector3(0.25), + position: [Float.random(in: -1.8...1.8), 1, -1] + ) + SpriteComponent(texture: textureAtlas[5, 7]) + EnemyComponent(health: 100, lifetime: 12) + collision + } + } +} + +@PlainSystem +struct EnemyLifetimeSystem { + @Query> + private var enemies + + @Res + private var deltaTime + + @Commands + private var commands + + init(world: World) { } + + func update(context: UpdateContext) async { + enemies.forEach { entity, enemy in + enemy.currentLifetime += deltaTime.deltaTime + + if enemy.wrappedValue.lifetime < enemy.currentLifetime { + commands.entity(entity.id).removeFromWorld() + } + } + } +} + +@PlainSystem +struct EnemyMovementSystem { + + @FilterQuery, With> + private var enemies + let speed: Float = 0.1 + + @Res + private var deltaTime + + init(world: World) { } + + func update(context: UpdateContext) { + enemies.forEach { transform in + transform.position.y -= speed * deltaTime.deltaTime + } + } +} + +@PlainSystem +struct EnemyExplosionSystem { + + let exposionAtlas: TextureAtlas + let explosionAudio: AudioResource + + init(world: World) { + do { + let image = try AssetsManager.loadSync( + Image.self, + at: "Resources/explosion.png", + from: .module + ).asset! + self.exposionAtlas = TextureAtlas(from: image, size: SizeInt(width: 32, height: 32)) + + self.explosionAudio = try AssetsManager.loadSync( + AudioResource.self, + at: "Resources/explosion-1.wav", + from: .module + ).asset! + } catch { + fatalError("Can't load assets \(error)") + } + } + + @Query + private var enemies + + @FilterQuery> + private var explosions + + @ResMut + private var score + + @Commands + private var commands + + func update(context: UpdateContext) async { + // Make expolosions + await enemies.forEach { entity, enemy, transform in + if enemy.health <= 0 { + score.score += 1 + + let texture = AnimatedTexture() + texture.framesPerSecond = 6 + texture.framesCount = 6 + texture.options = [] + texture[0] = self.exposionAtlas[0, 0] + texture[1] = self.exposionAtlas[1, 0] + texture[2] = self.exposionAtlas[2, 0] + texture[3] = self.exposionAtlas[3, 0] + texture[4] = self.exposionAtlas[4, 0] + texture[5] = self.exposionAtlas[5, 0] + + let explosion = context.world.spawn { + SpriteComponent(texture: texture) + transform + ExplosionComponent() + } + let controller = await explosion.prepareAudio(self.explosionAudio) + controller.volume = 0.4 + controller.play() + commands.entity(entity.id).removeFromWorld() + } + } + + // Remove explosions + explosions.forEach { entity, sprite in + guard let texture = sprite.texture?.asset as? AnimatedTexture else { + return + } + + if texture.isPaused { + commands.entity(entity.id).removeFromWorld() + } + } + } +} + +@PlainSystem +struct ScoreSystem { + @Query> + private var scores + + @Res + private var score + + @Local + private var container = TextAttributeContainer() + + init(world: World) { + container = TextAttributeContainer() + container.foregroundColor = .white + } + + func update(context: UpdateContext) async { + scores.forEach { text in + text.text = AttributedText("Score: \(score.score)", attributes: container) + } + } +} diff --git a/Assets/Examples/Input/GamepadExampleScene/GamepadExampleScene.swift b/Assets/Examples/Input/GamepadExampleScene/GamepadExampleScene.swift new file mode 100644 index 000000000..90c810cd5 --- /dev/null +++ b/Assets/Examples/Input/GamepadExampleScene/GamepadExampleScene.swift @@ -0,0 +1,113 @@ +import AdaEngine + +// Define a simple component to mark our player entity +@Component +struct PlayerComponent {} + +@main +struct GamepadExampleApp: App { + var body: some AppScene { + EmptyWindow() + .transformAppWorlds { appWorld in + appWorld.main.spawn( + "Camera", + bundle: OrthographicCameraBundle( + camera: Camera(), + transform: Transform(position: [0, 0, 0]) + ) + ) + + // Create a simple player entity + appWorld.main.spawn("Player") { + PlayerComponent() + Transform(scale: .init(0.5)) + SpriteComponent(tintColor: .red) + } + + // Add a system to process gamepad input + appWorld.main.addSystem(GamepadInputSystem.self) + } + .addPlugins(DefaultPlugins()) + .windowMode(.windowed) + } +} + +// Define a system to handle gamepad inputs +@PlainSystem +struct GamepadInputSystem { + + @FilterQuery, With> + private var playerQuery + + @Res + private var input + + @Res + private var deltaTime + + init(world: World) { } + + func update(context: UpdateContext) { + // Get all connected gamepad IDs + let gamepads = input.getConnectedGamepads() + + if gamepads.isEmpty { + // print("No gamepads connected.") // This might be too verbose for every frame + return + } + + for gamepad in gamepads { + if let info = gamepad.info { + // Only print connection info once or less frequently if needed + // For this example, printing it can be helpful for debugging. + print("Gamepad \(gamepad.gamepadId) (\(info.name ) - \(info.type ?? "N/A")) is connected.") + } + + // Check some common buttons + if gamepad.isGamepadButtonPressed(.a) { + print("Gamepad \(gamepad.gamepadId): Button A Pressed") + } + if gamepad.isGamepadButtonPressed(.b) { + print("Gamepad \(gamepad.gamepadId): Button B Pressed") + } + if gamepad.isGamepadButtonPressed(.leftShoulder) { + print("Gamepad \(gamepad.gamepadId): Left Shoulder Pressed") + } + + // Read some common axes + let leftStickX = gamepad.getAxisValue(.leftStickX) + let leftStickY = gamepad.getAxisValue(.leftStickY) + + if abs(leftStickX) > 0.1 || abs(leftStickY) > 0.1 { // Add a deadzone to avoid spam + print("Gamepad \(gamepad.gamepadId): Left Stick X: \(leftStickX.format(.fixed(precision: 2))), Y: \(leftStickY.format(.fixed(precision: 2)))") + } + + // Example of using axis value to move the player entity + playerQuery.forEach { transform in + // Assuming Y-axis from gamepad is inverted for typical 2D top-down movement (positive Y up) + transform.position.x += leftStickX * Float(deltaTime.deltaTime) * 2.0 // Adjust speed factor as needed + transform.position.y -= leftStickY * Float(deltaTime.deltaTime) * 2.0 // Inverted Y + } + + // Check for a specific button to trigger rumble (e.g., X button) + if gamepad.isGamepadButtonPressed(.x) { + print("Gamepad \(gamepad.gamepadId): Button X Pressed - Requesting Rumble") + gamepad.rumble(lowFrequency: 0.5, highFrequency: 0.75, duration: 0.5) + } + } + } +} + +// Helper for formatting float values in print statements +extension Float { + enum FormatStyle { // Renamed to avoid conflict with Foundation.FormatStyle if ever imported + case fixed(precision: Int) + } + + func format(_ style: FormatStyle) -> String { + switch style { + case .fixed(let precision): + return String(format: "%.\(precision)f", self) + } + } +} diff --git a/Sources/AdaEditor/Assets/Subscene.ascn b/Assets/Examples/Resources/Subscene.ascn similarity index 100% rename from Sources/AdaEditor/Assets/Subscene.ascn rename to Assets/Examples/Resources/Subscene.ascn diff --git a/Examples/Sources/scene_load/Resources/TestScene.ascn b/Assets/Examples/Resources/TestScene.ascn similarity index 100% rename from Examples/Sources/scene_load/Resources/TestScene.ascn rename to Assets/Examples/Resources/TestScene.ascn diff --git a/Sources/AdaEditor/Assets/TestTileMap.ldtk b/Assets/Examples/Resources/TestTileMap.ldtk similarity index 100% rename from Sources/AdaEditor/Assets/TestTileMap.ldtk rename to Assets/Examples/Resources/TestTileMap.ldtk diff --git a/Assets/Examples/Resources/WindlessSlopes.wav b/Assets/Examples/Resources/WindlessSlopes.wav new file mode 100644 index 000000000..f12b60093 Binary files /dev/null and b/Assets/Examples/Resources/WindlessSlopes.wav differ diff --git a/Assets/Examples/Resources/characters_packed.png b/Assets/Examples/Resources/characters_packed.png new file mode 100644 index 000000000..867a73a5b Binary files /dev/null and b/Assets/Examples/Resources/characters_packed.png differ diff --git a/Sources/AdaEditor/Assets/custom_material.glsl b/Assets/Examples/Resources/custom_material.glsl similarity index 100% rename from Sources/AdaEditor/Assets/custom_material.glsl rename to Assets/Examples/Resources/custom_material.glsl diff --git a/Assets/Examples/Resources/dog.png b/Assets/Examples/Resources/dog.png new file mode 100644 index 000000000..fec20762e Binary files /dev/null and b/Assets/Examples/Resources/dog.png differ diff --git a/Assets/Examples/Resources/explosion-1.wav b/Assets/Examples/Resources/explosion-1.wav new file mode 100644 index 000000000..9263c6b79 Binary files /dev/null and b/Assets/Examples/Resources/explosion-1.wav differ diff --git a/Assets/Examples/Resources/explosion-2.wav b/Assets/Examples/Resources/explosion-2.wav new file mode 100644 index 000000000..99c87ea41 Binary files /dev/null and b/Assets/Examples/Resources/explosion-2.wav differ diff --git a/Assets/Examples/Resources/explosion.png b/Assets/Examples/Resources/explosion.png new file mode 100644 index 000000000..c5cfd2db9 Binary files /dev/null and b/Assets/Examples/Resources/explosion.png differ diff --git a/Sources/AdaEditor/Assets/laserShoot.wav b/Assets/Examples/Resources/laserShoot.wav similarity index 100% rename from Sources/AdaEditor/Assets/laserShoot.wav rename to Assets/Examples/Resources/laserShoot.wav diff --git a/Assets/Examples/Resources/tiles.png b/Assets/Examples/Resources/tiles.png new file mode 100644 index 000000000..40fce07d0 Binary files /dev/null and b/Assets/Examples/Resources/tiles.png differ diff --git a/Assets/Examples/Resources/tiles_packed.png b/Assets/Examples/Resources/tiles_packed.png new file mode 100644 index 000000000..0da8986c6 Binary files /dev/null and b/Assets/Examples/Resources/tiles_packed.png differ diff --git a/Assets/Examples/Scene/LdtkTilemap/TilemapScene.swift b/Assets/Examples/Scene/LdtkTilemap/TilemapScene.swift new file mode 100644 index 000000000..380a80e2c --- /dev/null +++ b/Assets/Examples/Scene/LdtkTilemap/TilemapScene.swift @@ -0,0 +1,321 @@ +// +// TilemapScene.swift +// +// +// Created by v.prusakov on 5/4/24. +// + +import AdaEngine + +@main +struct LdtkTilemapApp: App { + var body: some AppScene { + EmptyWindow() + .addPlugins( + LdtkTilemapPlugin(), + DefaultPlugins() + ) + .windowMode(.windowed) + } +} + + +final class LdtkTilemapPlugin: Plugin { + func setup(in app: borrowing AppWorlds) { + app.main.spawn( + bundle: OrthographicCameraBundle( + camera: Camera() + .setBackgroundColor(Color(135/255, 206/255, 235/255, 1)) + .setOrthographicScale(10.5) + ) + ) + + var transform = Transform() + transform.position.y = -0.5 + transform.scale = Vector3(0.5) + + do { + let tileMap = try AssetsManager.loadSync( + LDtk.TileMap.self, + at: "Resources/TestTileMap.ldtk", + from: .module + ).asset! + tileMap.delegate = self + tileMap.loadLevel(at: 0) + + app.main.spawn { + TileMapComponent(tileMap: tileMap) + NoFrustumCulling() + transform + } + } catch { + fatalError("Failed to load \(error)") + } + + app.main.addSystem(CamMovementSystem.self) + } +} + +extension LdtkTilemapPlugin: TileMapDelegate { + func tileMap( + _ tileMap: LDtk.TileMap, + needsUpdate entity: Entity, + from instance: LDtk.EntityInstance, + in tileSource: LDtk.EntityTileSource + ) { + + } +} + +//final class TilemapScene: Scene, @unchecked Sendable { +// +// enum TileAtlasCoordinates { +// static let topLeft: PointInt = [1, 5] +// static let topRight: PointInt = [3, 5] +// static let bottomLeft: PointInt = [1, 7] +// static let bottomRight: PointInt = [3, 7] +// static let middleTop: PointInt = [2, 5] +// static let middleBottom: PointInt = [2, 7] +// +// static let first: PointInt = [1, 6] +// static let last: PointInt = [3, 6] +// static let plain: PointInt = [2, 6] +// +// static let riverStart: PointInt = [14, 1] +// static let riverBody: PointInt = [14, 2] +// static let riverEnd: PointInt = [14, 3] +// } +// +// override func sceneDidMove(to view: SceneView) { +//// if FileSystem.current.itemExists(at: URL(filePath: "/Users/vprusakov/Downloads/tilemap.res")) { +//// loadIfNeeded() +//// } else { +// save() +//// } +// } +// +// private func loadIfNeeded() { +// let tileMap = try! AssetsManager.loadSync( +// TileMap.self, +// at: "/Users/vprusakov/Downloads/tilemap.res", +// from: .editor +// ).asset +// +// let cameraEntity = OrthographicCamera() +// cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) +// cameraEntity.camera.clearFlags = .solid +// cameraEntity.camera.orthographicScale = 1.5 +// +// self.world.addEntity(cameraEntity) +// +// var transform = Transform() +// transform.position.y = -0.5 +// transform.scale = Vector3(0.5) +// +// let tilemapEnt = Entity { +// TileMapComponent(tileMap: tileMap) +// NoFrustumCulling() +// transform +// } +// +// self.world.addEntity(tilemapEnt) +// self.world.addSystem(CamMovementSystem.self) +// } +// +// // swiftlint:disable:next function_body_length +// private func save() { +// let tileMap = TileMap() +// +// let image = try! AssetsManager.loadSync( +// Image.self, +// at: "Assets/tiles_packed.png" +// ).asset +// let source = TextureAtlasTileSource(from: image, size: [18, 18]) +// +// source.createTile(for: TileAtlasCoordinates.topLeft) +// source.createTile(for: TileAtlasCoordinates.topRight) +// source.createTile(for: TileAtlasCoordinates.bottomLeft) +// source.createTile(for: TileAtlasCoordinates.bottomRight) +// source.createTile(for: TileAtlasCoordinates.middleTop) +// source.createTile(for: TileAtlasCoordinates.middleBottom) +// +// source.createTile(for: TileAtlasCoordinates.last) +// source.createTile(for: TileAtlasCoordinates.first) +// source.createTile(for: TileAtlasCoordinates.plain) +// +// // Add animated river +// source.createTile(for: TileAtlasCoordinates.riverStart) +// .setAnimationFrameColumns(2) +// .setAnimationFrameDuration(0.5) +// +// source.createTile(for: TileAtlasCoordinates.riverBody) +// .setAnimationFrameColumns(2) +// .setAnimationFrameDuration(0.5) +// +// source.createTile(for: TileAtlasCoordinates.riverEnd) +// .setAnimationFrameColumns(2) +// .setAnimationFrameDuration(0.5) +// +// let sourceId = tileMap.tileSet.addTileSource(source) +// +// let xRange = 0..<15 +// let yRange = 0..<6 +// +// for x in xRange { +// for y in yRange { +// let atlasCoordinates = getCoordinates(for: x, y: y, maxX: xRange.upperBound, maxY: yRange.upperBound) +// +// tileMap.layers[0].setCell( +// at: [x, y], +// sourceId: sourceId, +// atlasCoordinates: atlasCoordinates +// ) +// } +// } +// +// for y in yRange { +// let isStart = y == 0 +// let isEnd = y == yRange.upperBound - 1 +// +// var coordinates: PointInt = [0, 0] +// +// if isStart { +// coordinates = TileAtlasCoordinates.riverEnd +// } else if isEnd { +// coordinates = TileAtlasCoordinates.riverStart +// } else { +// coordinates = TileAtlasCoordinates.riverBody +// } +// +// tileMap.layers[0].setCell( +// at: [0, y], +// sourceId: sourceId, +// atlasCoordinates: coordinates +// ) +// } +// +// let cameraEntity = OrthographicCamera() +// cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) +// cameraEntity.camera.clearFlags = .solid +// cameraEntity.camera.orthographicScale = 1.5 +// +// self.world.addEntity(cameraEntity) +// +// var transform = Transform() +// transform.position.y = -0.5 +// transform.scale = Vector3(0.5) +// +// let tilemapEnt = Entity { +// TileMapComponent(tileMap: tileMap) +// NoFrustumCulling() +// transform +// } +// +// self.world.addEntity(tilemapEnt) +// self.world.addSystem(CamMovementSystem.self) +// +// Task { @AssetActor in +// do { +// try await AssetsManager.save(tileMap, at: "/Users/vprusakov/Downloads", name: "tilemap") +// } catch { +// print("Failed", error) +// } +// } +// } +// +// func getCoordinates(for x: Int, y: Int, maxX: Int, maxY: Int) -> PointInt { +// let isFirst = x == 0 +// let isLast = x == maxX - 1 +// +// let bottom = y == 0 +// let top = y == maxY - 1 +// +// if isFirst && top { +// return TileAtlasCoordinates.topLeft +// } +// +// if isFirst && bottom { +// return TileAtlasCoordinates.bottomLeft +// } +// +// if isLast && top { +// return TileAtlasCoordinates.topRight +// } +// +// if isLast && bottom { +// return TileAtlasCoordinates.bottomRight +// } +// +// if top { +// return TileAtlasCoordinates.middleTop +// } +// +// if bottom { +// return TileAtlasCoordinates.middleBottom +// } +// +// if isFirst { +// return TileAtlasCoordinates.first +// } +// +// if isLast { +// return TileAtlasCoordinates.last +// } +// +// return TileAtlasCoordinates.plain +// } +//} + +@PlainSystem +struct CamMovementSystem { + + @Query, Ref> + private var cameras + + @Query + private var tileMaps + + @Res + private var input + + @Res + private var deltaTime + + init(world: World) { } + + func update(context: UpdateContext) { + let (camera, cameraTransform) = cameras.first! + let tileMap = tileMaps.first! + + if input.isKeyPressed(.m) { + tileMap.tileMap.layers[0].isEnabled.toggle() + } + + let speed: Float = input.isKeyPressed(.space) ? 5 : 2 + let speedNormalized: Float = speed * deltaTime.deltaTime + + if input.isKeyPressed(.w) { + cameraTransform.position.y += speedNormalized + } + + if input.isKeyPressed(.s) { + cameraTransform.position.y -= speedNormalized + } + + if input.isKeyPressed(.a) { + cameraTransform.position.x -= speedNormalized + } + + if input.isKeyPressed(.d) { + cameraTransform.position.x += speedNormalized + } + + if input.isKeyPressed(.arrowUp) { + camera.orthographicScale -= speedNormalized + } + + if input.isKeyPressed(.arrowDown) { + camera.orthographicScale += speedNormalized + } + } +} diff --git a/Examples/Sources/scene_load/scene_load_app.swift b/Assets/Examples/Scene/scene_load/scene_load_app.swift similarity index 72% rename from Examples/Sources/scene_load/scene_load_app.swift rename to Assets/Examples/Scene/scene_load/scene_load_app.swift index 373664840..b1c792441 100644 --- a/Examples/Sources/scene_load/scene_load_app.swift +++ b/Assets/Examples/Scene/scene_load/scene_load_app.swift @@ -10,10 +10,10 @@ struct SceneLoadApp: App { ComponentC.registerComponent() // Register system to be able to load it from the scene file - PlayerSystem.registerSystem() +// PlayerSystem.registerSystem() } - var scene: some AppScene { + var body: some AppScene { WindowGroup { SceneLoadView() } @@ -36,10 +36,10 @@ struct SceneLoadView: View { Task { do { // Load scene from the bundle in `Resources` directory. - let scene: Scene = try await AssetsManager.load("TestScene.ascn", from: Bundle.module) - let entities = scene.world.getEntities() - print("scene: \(scene.name) entities: \(entities.count)") - print("entities:", entities.map { + let scene = try await AssetsManager.load(Scene.self, at: "TestScene.ascn", from: Bundle.module) + let entities = scene.asset.world.getEntities() + print("scene: \(scene.asset.name) entities: \(entities.count)") + print("entities:", entities.map { "\n- name: \($0.name)\n components: -\n\($0.components)\n" }) } catch { @@ -50,24 +50,22 @@ struct SceneLoadView: View { private func saveScene() { let scene = Scene(name: "TestScene") - scene.world.addEntity( - Entity(name: "Player") { - Transform(position: [10, 10, 0]) - ComponentA(value: 10) - ComponentB(value: "Hello", ignoredValue: .init(wrappedValue: 10)) - } - ) - scene.world.addEntity(Entity(name: "Enemy") { + scene.world.spawn("Player") { + Transform(position: [10, 10, 0]) + ComponentA(value: 10) + ComponentB(value: "Hello", ignoredValue: .init(wrappedValue: 10)) + } + scene.world.spawn("Enemy") { Transform(position: [20, 20, 0]) ComponentA(value: 20) ComponentB(value: "World", ignoredValue: .init(wrappedValue: 20)) - }) - scene.world.addEntity(Entity(name: "Enemy") { + } + scene.world.spawn("Enemy") { Transform(position: [30, 30, 0]) ComponentA(value: 30) ComponentC(value: "Enemy 2") ComponentB(value: "Enemy", ignoredValue: .init(wrappedValue: 30)) - }) + } scene.world.addSystem(PlayerSystem.self) @@ -103,16 +101,18 @@ struct ComponentC { } -extension Int: @retroactive DefaultValue { +extension Int: DefaultValue { public static var defaultValue: Int { return 0 } } -struct PlayerSystem: System { +@PlainSystem +struct PlayerSystem { init(world: World) { } - func update(context: UpdateContext) { } -} + func update(context: UpdateContext) async { + } +} diff --git a/Assets/Examples/UI/AnimatedTextRenderer/AnimatedTextRendererApp.swift b/Assets/Examples/UI/AnimatedTextRenderer/AnimatedTextRendererApp.swift new file mode 100644 index 000000000..3aee3d24f --- /dev/null +++ b/Assets/Examples/UI/AnimatedTextRenderer/AnimatedTextRendererApp.swift @@ -0,0 +1,85 @@ +// +// AnimatedTextRendererApp.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 09.12.2025. +// + +import AdaEngine + +@main +struct AnimatedTextRendererApp: App { + var body: some AppScene { + WindowGroup { + ContentView() + } + } +} + +struct ContentView: View { + @State private var isAnimated: Bool = false + @State private var timeOffset: Double = 0.0 + + var body: some View { + Text("Some wave") + .textRendered(AnimatedSineWaveOffsetRender(timeOffset: timeOffset)) + .onAppear { + isAnimated = true + } + } +} + +extension Text.Layout { + var runs: some RandomAccessCollection { + flatMap { line in + line + } + } + + var flattenedRuns: some RandomAccessCollection { + runs.flatMap { $0 } + } +} + +struct AnimatedSineWaveOffsetRender: TextRenderer { + let timeOffset: Double // Time offset + + init(timeOffset: Double) { + self.timeOffset = timeOffset + } + + func draw(layout: Text.Layout, in context: inout UIGraphicsContext) { + let count = layout.flattenedRuns.count // Count all RunSlices in the text layout + let width = layout.first?.typographicBounds.rect.width ?? 0 // Get the width of the text line + let height = layout.first?.typographicBounds.rect.height ?? 0 // Get the height of the text line + // Iterate through each RunSlice and its index + for (index, glyph) in layout.flattenedRuns.enumerated() { + // Calculate the sine wave offset for the current character + let offset = animatedSineWaveOffset( + forCharacterAt: index, + amplitude: Double(height) / 2, // Set amplitude to half the line height + wavelength: Double(width), + phaseOffset: timeOffset, + totalCharacters: count + ) + // Create a copy of the context and translate it + var copy = context + copy.translateBy(x: 0, y: Float(offset)) + // Draw the current RunSlice in the modified context + copy.draw(glyph) + } + + func animatedSineWaveOffset( + forCharacterAt index: Int, + amplitude: Double, + wavelength: Double, + phaseOffset: Double, + totalCharacters: Int + ) -> Double { + let x = Double(index) + let position = (x / Double(totalCharacters)) * wavelength + let radians = ((position + phaseOffset) / wavelength) * 2 * .pi + return Math.sin(radians) * amplitude + } + } +} diff --git a/Assets/Examples/UI/UITestScene/UITestScene.swift b/Assets/Examples/UI/UITestScene/UITestScene.swift new file mode 100644 index 000000000..e5ce67ff6 --- /dev/null +++ b/Assets/Examples/UI/UITestScene/UITestScene.swift @@ -0,0 +1,29 @@ +// +// UITesScene.swift +// AdaEngine +// +// Created by vladislav.prusakov on 19.08.2024. +// + +import AdaEngine + +@main +struct UITestSceneApp: App { + var body: some AppScene { + WindowGroup { + SomeContent() + } + .addPlugins(DefaultPlugins()) + .windowMode(.windowed) + } +} + +struct SomeContent: View { + var body: some View { + VStack { + Color.blue + + Color.green + } + } +} diff --git a/BUILD.bazel b/BUILD.bazel index 54e990834..6e7bbd796 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,5 +1,4 @@ load("@bazel_gazelle//:def.bzl", "gazelle", "gazelle_binary") -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_compiler_plugin") load("@cgrindel_bazel_starlib//bzltidy:defs.bzl", "tidy") package(default_visibility = ["//visibility:public"]) @@ -16,9 +15,6 @@ alias( actual = "//Sources/AdaEditor", ) -# Swift Package Manager -# gazelle:exclude .build - gazelle_binary( name = "gazelle_bin", languages = [ diff --git a/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb8789700..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 532cd729c..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/Contents.json b/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/EmbeddableView/EmbeddableView/Assets/characters_packed.png b/Examples/EmbeddableView/EmbeddableView/Assets/characters_packed.png deleted file mode 100644 index 9dc045db7..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Assets/characters_packed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:1702606cb8510ac8454dd93204f3f460d06a0d5071b080c86e856bb684abb929 -size 1973 diff --git a/Examples/EmbeddableView/EmbeddableView/Assets/tiles_packed.png b/Examples/EmbeddableView/EmbeddableView/Assets/tiles_packed.png deleted file mode 100644 index 350484575..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Assets/tiles_packed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:0d58b43a0e6a6818a76c6dbba176299cb10538aab2ee530bce9e856702d956fa -size 5811 diff --git a/Examples/EmbeddableView/EmbeddableView/BUILD.bazel b/Examples/EmbeddableView/EmbeddableView/BUILD.bazel deleted file mode 100644 index c63ee9772..000000000 --- a/Examples/EmbeddableView/EmbeddableView/BUILD.bazel +++ /dev/null @@ -1,11 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "EmbeddableView", - srcs = [ - "ContentView.swift", - "EmbeddableViewApp.swift", - ], - module_name = "EmbeddableView", - visibility = ["//visibility:public"], -) diff --git a/Examples/EmbeddableView/EmbeddableView/ContentView.swift b/Examples/EmbeddableView/EmbeddableView/ContentView.swift deleted file mode 100644 index 02b267c89..000000000 --- a/Examples/EmbeddableView/EmbeddableView/ContentView.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// ContentView.swift -// EmbeddableView -// -// Created by v.prusakov on 1/9/23. -// - -import SwiftUI -import class AdaEngine.Scene -import class AdaEngine.EventManager -import protocol AdaEngine.Cancellable - -struct ContentView: View { - - @State private var scene: Scene? - @State private var counter: Int = 0 - @State private var token: Cancellable? - - let builder = GameScene2D() - - var body: some View { - VStack { - if let scene = self.scene { - EngineView(scene: scene) - .edgesIgnoringSafeArea(.all) - } else { - - Spacer() - Button("Start the game") { - self.scene = try! builder.makeScene() - } - Spacer() - } - - if self.scene != nil { - Button("Restart") { - self.scene = nil - self.counter = 0 - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.scene = try! builder.makeScene() - } - } - } - - Spacer() - - Text("User Score: \(counter)") - .font(.title) - .padding(.all, 16) - } - .onAppear { - self.token = EventManager.default.subscribe(for: UserScoreEvent.self) { event in - self.counter += 1 - } - } - #if os(macOS) - .frame(width: 800, height: 600) - #endif - } -} diff --git a/Examples/EmbeddableView/EmbeddableView/EmbeddableView.entitlements b/Examples/EmbeddableView/EmbeddableView/EmbeddableView.entitlements deleted file mode 100644 index f2ef3ae02..000000000 --- a/Examples/EmbeddableView/EmbeddableView/EmbeddableView.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - - diff --git a/Examples/EmbeddableView/EmbeddableView/EmbeddableViewApp.swift b/Examples/EmbeddableView/EmbeddableView/EmbeddableViewApp.swift deleted file mode 100644 index 8d1a7ccca..000000000 --- a/Examples/EmbeddableView/EmbeddableView/EmbeddableViewApp.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// EmbeddableViewApp.swift -// EmbeddableView -// -// Created by v.prusakov on 1/9/23. -// - -import SwiftUI - -@main -struct EmbeddableViewApp: App { - - var body: some Scene { - WindowGroup { - ContentView() - } - } -} diff --git a/Examples/EmbeddableView/EmbeddableView/Engine/BUILD.bazel b/Examples/EmbeddableView/EmbeddableView/Engine/BUILD.bazel deleted file mode 100644 index 7815b44e6..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Engine/BUILD.bazel +++ /dev/null @@ -1,12 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "Engine", - srcs = [ - "EngineView.swift", - "GameScene.swift", - ], - module_name = "Engine", - visibility = ["//visibility:public"], - deps = ["//Sources/AdaEngineEmbeddable"], -) diff --git a/Examples/EmbeddableView/EmbeddableView/Engine/EngineView.swift b/Examples/EmbeddableView/EmbeddableView/Engine/EngineView.swift deleted file mode 100644 index dd126e873..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Engine/EngineView.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// EngineView.swift -// EmbeddableView -// -// Created by v.prusakov on 1/9/23. -// - -import SwiftUI -import AdaEngineEmbeddable -import AdaEngine - -struct EngineView { - let scene: AdaEngine.Scene -} - -#if os(iOS) -extension EngineView: UIViewRepresentable { - - func makeUIView(context: Context) -> AEView { - AEView(scene: self.scene, frame: .zero) - } - - func updateUIView(_ uiView: AEView, context: Context) { } -} -#endif - -#if os(macOS) -extension EngineView: NSViewRepresentable { - func makeNSView(context: Context) -> some NSView { - AEView(scene: self.scene, frame: .zero) - } - - func updateNSView(_ nsView: NSViewType, context: Context) { } -} -#endif diff --git a/Examples/EmbeddableView/EmbeddableView/Engine/GameScene.swift b/Examples/EmbeddableView/EmbeddableView/Engine/GameScene.swift deleted file mode 100644 index 76a9d1458..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Engine/GameScene.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// GameScene.swift -// EmbeddableView -// -// Created by v.prusakov on 1/9/23. -// - -import AdaEngine -#if os(iOS) -import UIKit -#elseif os(macOS) -import AppKit -#endif - -//class GameSceneBuilder { -// func makeScene() -> Scene { -// let scene = Scene() -// -// var transform = Transform() -// transform.scale = [10, 10, 10] -// -// let untexturedEntity = Entity(name: "Background") -// untexturedEntity.components += SpriteComponent(tintColor: Color(135/255, 206/255, 235/255, 1)) -// untexturedEntity.components += transform -// scene.addEntity(untexturedEntity) -// -// transform.scale = [0.12, 0.12, 0.12] -// -// let player = Entity(name: "Player") -// player.components += SpriteComponent(tintColor: Color.red) -// player.components += transform -// scene.addEntity(player) -// -// let userEntity = Entity(name: "camera") -// let camera = Camera() -// camera.projection = .orthographic -// camera.isPrimal = true -// userEntity.components += camera -// scene.addEntity(userEntity) -// -// return scene -// } -//} - -final class PlayerComponent: ScriptableComponent { - - @RequiredComponent var body: PhysicsBody2DComponent - - override func onUpdate(_ deltaTime: AdaEngine.TimeInterval) { - if Input.isKeyPressed(.space) { - body.applyLinearImpulse([0, 1], point: .zero, wake: true) - } - } - - override func onEvent(_ events: Set) { - for event in events { - if let touch = event as? TouchEvent { - if touch.phase == .moved { - body.applyLinearImpulse([0, 1], point: .zero, wake: true) - } - } - } - } -} - -@Component -struct TubeComponent { } - -struct TubeMovementSystem: System { - - static let tubeQuery = EntityQuery( - where: .has(TubeComponent.self) && .has(Transform.self) - ) - - init(world: World) { } - - func update(context: UpdateContext) { - context.world.performQuery(Self.tubeQuery).forEach { entity in - var transform = entity.components[Transform.self]! - transform.position.x -= 2 * context.deltaTime - entity.components += transform - } - } -} - -struct TubeDestoryerSystem: System { - static let tubeQuery = EntityQuery( - where: .has(TubeComponent.self) && .has(Transform.self) - ) - - init(world: World) { } - - func update(context: UpdateContext) { - context.world.performQuery(Self.tubeQuery).forEach { entity in - let transform = entity.components[Transform.self]! - - if transform.position.x < -4 { - entity.removeFromScene() - } - } - } -} - -class TubeSpawnerSystem: System { - - var lastSpawnTime: TimeInterval = 0 - var counter: TimeInterval = 0 - - required init(world: World) { } - - func update(context: UpdateContext) { - counter += context.deltaTime - - if lastSpawnTime < counter { - self.lastSpawnTime = counter + 3 - - var transform = Transform() - transform.scale = [0.4, 1, 1] - let position = Vector3(x: 4, y: Float.random(in: 0.4 ... 1.2), z: 0) - transform.position = position - - self.spawnTube(in: context.scene, transform: transform, isUp: true) - transform.position.y -= 1.5 - - self.spawnTube(in: context.scene, transform: transform, isUp: false) - } - } - - private func spawnTube(in scene: Scene, transform: Transform, isUp: Bool) { - let tube = Entity(name: "Tube") - tube.components += TubeComponent() - tube.components += SpriteComponent(tintColor: isUp ? .green : .blue) - tube.components += transform - tube.components += Collision2DComponent( - shapes: [ - .generateBox(width: 1, height: 1) - ], - mode: .trigger - ) - - scene.addEntity(tube) - } -} - -struct UserScoreEvent: Event {} - -final class GameScene2D { - - var collision: Cancellable! - var fpsCounter: Cancellable! - - let textureAtlas: TextureAtlas - let characterAtlas: TextureAtlas - - init() { - do { - let tiles = try Image(contentsOf: Bundle.main.url(forResource: "tiles_packed", withExtension: "png")!) - let charactersTiles = try Image(contentsOf: Bundle.main.url(forResource: "characters_packed", withExtension: "png")!) - - self.textureAtlas = TextureAtlas(from: tiles, size: [18, 18]) - self.characterAtlas = TextureAtlas(from: charactersTiles, size: [20, 23], margin: [4, 1]) - } catch { - fatalError(error.localizedDescription) - } - } - - func makeScene() throws -> Scene { - let scene = Scene() - - // DEBUG - - scene.debugOptions = [.showPhysicsShapes] - - self.makeBackground(for: scene) - self.makePlayer(for: scene) - self.makeGround(for: scene) - self.collisionHandler(for: scene) - self.fpsCounter(for: scene) - - let userEntity = Entity(name: "camera") - let camera = Camera() - camera.projection = .orthographic - camera.isPrimal = true - userEntity.components += camera - scene.addEntity(userEntity) - - world.addSystem(TubeMovementSystem.self) - world.addSystem(TubeSpawnerSystem.self) - world.addSystem(TubeDestoryerSystem.self) - - return scene - } - - private func collisionHandler(for scene: Scene) { - self.collision = scene.subscribe(to: CollisionEvents.Began.self) { event in - if event.entityA.name == "Player" && (event.entityB.name == "Tube") { -// event.entityA.scene?.removeEntity(event.entityA) - print("collide with tube") - - EventManager.default.send(UserScoreEvent()) -// self.gameOver() - } - } - } - - private func makeBackground(for scene: Scene) { - var transform = Transform() - transform.scale = [10, 10, 10] - - let untexturedEntity = Entity(name: "Background") - untexturedEntity.components += SpriteComponent(tintColor: Color(135/255, 206/255, 235/255, 1)) - untexturedEntity.components += transform - scene.addEntity(untexturedEntity) - } - - private func makePlayer(for scene: Scene) { - - var transform = Transform() - transform.scale = [0.2, 0.2, 0.2] - - let playerTexture = AnimatedTexture() - playerTexture.framesPerSecond = 5 - playerTexture.framesCount = 2 - playerTexture[0] = self.characterAtlas[0, 0] - playerTexture[1] = self.characterAtlas[1, 0] - - let playerEntity = Entity(name: "Player") - playerEntity.components += SpriteComponent(texture: playerTexture) - playerEntity.components += transform - playerEntity.components += PhysicsBody2DComponent( - shapes: [ - .generateBox(width: transform.scale.x, height: transform.scale.y) - ], - mass: 1, - mode: .dynamic - ) - playerEntity.components += PlayerComponent() - scene.addEntity(playerEntity) - } - - private func makeGround(for scene: Scene) { - var transform = Transform() - transform.scale = [3, 0.19, 0.19] - transform.position.y = -4.9 - - let untexturedEntity = Entity(name: "Ground") - untexturedEntity.components += SpriteComponent(texture: self.textureAtlas[0, 0]) - untexturedEntity.components += transform - untexturedEntity.components += PhysicsBody2DComponent( - shapes: [ - .generateBox(width: 1, height: 1).offsetBy(x: 0, y: 1) - ], - mass: 0, - mode: .static - ) - - scene.addEntity(untexturedEntity) - } - - private func gameOver() { - print("Game Over") - } - - private func fpsCounter(for scene: Scene) { - self.fpsCounter = EventManager.default.subscribe(for: EngineEvents.FramesPerSecondEvent.self, completion: { event in -// print("FPS", event.framesPerSecond) - }) - } -} diff --git a/Examples/EmbeddableView/EmbeddableView/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/EmbeddableView/EmbeddableView/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Examples/EmbeddableView/EmbeddableView/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Package.swift b/Examples/Package.swift deleted file mode 100644 index 8748fec28..000000000 --- a/Examples/Package.swift +++ /dev/null @@ -1,28 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "Examples", - platforms: [ - .iOS("16.0"), - .macOS("14.0"), - .watchOS("9.0"), - .tvOS("16.0"), - .visionOS("1.0") - ], - dependencies: [ - .package(path: "../") - ], - targets: [ - .executableTarget( - name: "scene_load", - dependencies: [ - "AdaEngine" - ], - resources: [ - .copy("Resources") - ], - ) - ] -) \ No newline at end of file diff --git a/Examples/Sources/GamepadExampleScene/GamepadExampleScene.swift b/Examples/Sources/GamepadExampleScene/GamepadExampleScene.swift deleted file mode 100644 index d6b0524e5..000000000 --- a/Examples/Sources/GamepadExampleScene/GamepadExampleScene.swift +++ /dev/null @@ -1,116 +0,0 @@ -import AdaEngine - -// Define a simple component to mark our player entity -struct PlayerComponent: Component {} - -class GamepadExampleScene { - - // Method to create and configure the scene - static func makeScene() -> Scene { - let scene = Scene() - - // Setup a camera - let cameraEntity = Entity(name: "Camera") - let camera = Camera() - camera.isOrthographic = true - camera.projection = .orthographic(size: 10, near: 0.1, far: 100) - cameraEntity.components.add(camera) - cameraEntity.components.add(Transform(position: [0, 0, 10])) - scene.addEntity(cameraEntity) - - // Create a simple player entity - let playerEntity = Entity(name: "Player") - playerEntity.components.add(PlayerComponent()) - playerEntity.components.add(Transform(scale: [0.5, 0.5, 0.5])) // So it's visible - // Add a Sprite or some visual component if easy, otherwise Transform is fine for console output. - // For simplicity, we'll focus on console output for inputs. - scene.addEntity(playerEntity) - - // Add a system to process gamepad input - let system = GamepadInputSystem(scene: scene) // Pass scene to system initializer - scene.addSystem(system) - - print("Gamepad Example Scene Initialized. Connect a gamepad to see input.") - - return scene - } -} - -// Define a system to handle gamepad inputs -struct GamepadInputSystem: System { - - static let playerQuery = EntityQuery(where: .has(PlayerComponent.self) && .has(Transform.self)) - - init(scene: Scene) { } - - func update(context: UpdateContext) { - // Get all connected gamepad IDs - let gamepadIds = Input.getConnectedGamepadIds() - - if gamepadIds.isEmpty { - // print("No gamepads connected.") // This might be too verbose for every frame - return - } - - for id in gamepadIds { - if Input.isGamepadConnected(gamepadId: id) { - if let info = Input.getGamepadInfo(gamepadId: id) { - // Only print connection info once or less frequently if needed - // For this example, printing it can be helpful for debugging. - print("Gamepad \(id) (\(info.name ) - \(info.type ?? "N/A")) is connected.") - } - - // Check some common buttons - if Input.isGamepadButtonPressed(id, button: .a) { - print("Gamepad \(id): Button A Pressed") - } - if Input.isGamepadButtonPressed(id, button: .b) { - print("Gamepad \(id): Button B Pressed") - } - if Input.isGamepadButtonPressed(id, button: .leftShoulder) { - print("Gamepad \(id): Left Shoulder Pressed") - } - - // Read some common axes - let leftStickX = Input.getGamepadAxisValue(id, axis: .leftStickX) - let leftStickY = Input.getGamepadAxisValue(id, axis: .leftStickY) - - if abs(leftStickX) > 0.1 || abs(leftStickY) > 0.1 { // Add a deadzone to avoid spam - print("Gamepad \(id): Left Stick X: \(leftStickX.format(.fixed(precision: 2))), Y: \(leftStickY.format(.fixed(precision: 2)))") - } - - // Example of using axis value to move the player entity - context.scene.performQuery(Self.playerQuery).forEach { entity in - var transform = entity.components[Transform.self]! - // Assuming Y-axis from gamepad is inverted for typical 2D top-down movement (positive Y up) - transform.position.x += leftStickX * Float(context.deltaTime) * 2.0 // Adjust speed factor as needed - transform.position.y -= leftStickY * Float(context.deltaTime) * 2.0 // Inverted Y - entity.components[Transform.self] = transform - } - - // Check for a specific button to trigger rumble (e.g., X button) - if Input.isGamepadButtonPressed(id, button: .x) { - print("Gamepad \(id): Button X Pressed - Requesting Rumble") - Input.rumbleGamepad(gamepadId: id, lowFrequency: 0.5, highFrequency: 0.75, duration: 0.5) - } - - } else { - print("Gamepad \(id) was in list but now reports disconnected.") - } - } - } -} - -// Helper for formatting float values in print statements -extension Float { - enum FormatStyle { // Renamed to avoid conflict with Foundation.FormatStyle if ever imported - case fixed(precision: Int) - } - - func format(_ style: FormatStyle) -> String { - switch style { - case .fixed(let precision): - return String(format: "%.\(precision)f", self) - } - } -} diff --git a/MODULE.bazel b/MODULE.bazel index 2d6df9355..65cab4032 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -15,7 +15,7 @@ single_version_override( version = "2.8.2", ) -bazel_dep(name = "cgrindel_bazel_starlib", version = "0.23.0") +bazel_dep(name = "cgrindel_bazel_starlib", version = "0.27.0") bazel_dep( name = "rules_apple", version = "3.16.1", @@ -26,7 +26,7 @@ bazel_dep(name = "apple_support", version = "1.17.1", repo_name = "build_bazel_a bazel_dep(name = "rules_xcodeproj", version = "2.12.0", dev_dependency = True, repo_name = "rules_xcodeproj") bazel_dep(name = "swift-syntax", version = "600.0.0", repo_name = "SwiftSyntax") -bazel_dep(name = "zlib", version = "1.3.1") +bazel_dep(name = "zlib", version = "1.3.1.bcr.3") bazel_dep( name = "bazel_skylib_gazelle_plugin", @@ -51,8 +51,8 @@ swift_deps = use_extension( ) swift_deps.from_package( declare_swift_deps_info = True, - resolved = "Package.resolved", - swift = "Package.swift", + resolved = "//:Package.resolved", + swift = "//:Package.swift", ) use_repo( swift_deps, diff --git a/Modules/box2d/BUILD.bazel b/Modules/box2d/BUILD.bazel index 4256800bf..c747a7956 100644 --- a/Modules/box2d/BUILD.bazel +++ b/Modules/box2d/BUILD.bazel @@ -1,8 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift_interop_hint.bzl", "swift_interop_hint") + cc_library( name = "box2d", srcs = glob(["src/*.c"]), - hdrs = glob(["src/*.h"]), - includes = ["include/include"], - tags = ["swift_module=box2d"], + hdrs = glob([ + "src/*.h", + "include/box2d/*.h", + ]), + includes = ["include"], visibility = ["//visibility:public"], + aspect_hints = ["box2d_cc"], ) + +swift_interop_hint( + name = "box2d_cc", + module_name = "box2d", + visibility = ["//visibility:public"] +) \ No newline at end of file diff --git a/Modules/libpng/BUILD b/Modules/libpng/BUILD index 6447ece1f..46d05a94e 100644 --- a/Modules/libpng/BUILD +++ b/Modules/libpng/BUILD @@ -26,11 +26,9 @@ cc_library( ], "//conditions:default": [], }), - hdrs = [ - "Sources/libpng/include/libpng.h", - "Sources/libpng/include/png.h", - "Sources/libpng/include/pngconf.h", - ], + hdrs = glob([ + "Sources/libpng/include/*.h" + ]), defines = select({ "@platforms//cpu:arm64": [ "PNG_ARM_NEON_OPT=2", diff --git a/Modules/libpng/MODULE.bazel b/Modules/libpng/MODULE.bazel index 6108dee91..6c37b6215 100644 --- a/Modules/libpng/MODULE.bazel +++ b/Modules/libpng/MODULE.bazel @@ -11,4 +11,4 @@ module( repo_name = "AdaEngine" ) -bazel_dep(name = "zlib", version = "1.3.1") \ No newline at end of file +bazel_dep(name = "zlib", version = "1.3.1.bcr.3") \ No newline at end of file diff --git a/Modules/msdf-atlas-gen/BUILD b/Modules/msdf-atlas-gen/BUILD index d47c940e6..0cfcea62a 100644 --- a/Modules/msdf-atlas-gen/BUILD +++ b/Modules/msdf-atlas-gen/BUILD @@ -90,7 +90,6 @@ cc_library( "MSDFGEN_USE_CPP11", ], includes = ["msdfgen"], - aspect_hints = ["msdfgen_cxx"], deps = [ ":freetype", ":tinyxml", @@ -113,4 +112,10 @@ cc_library( visibility = ["//visibility:public"], aspect_hints = ["msdf-atlas-gen_cxx"], deps = [":msdfgen"], +) + +swift_interop_hint( + name = "msdf-atlas-gen_cxx", + module_name = "msdf-atlas-gen", + visibility = ["//visibility:public"] ) \ No newline at end of file diff --git a/Package.swift b/Package.swift index 6894fa1ce..10a9bce48 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.0 +// swift-tools-version: 6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -41,6 +41,10 @@ var products: [Product] = [ name: "AdaECS", targets: ["AdaECS"] ), + .library( + name: "AdaRender", + targets: ["AdaRender"] + ), .library( name: "AdaEngineEmbeddable", targets: ["AdaEngineEmbeddable"] @@ -81,6 +85,12 @@ products.append(ios) var commonPlugins: [Target.PluginUsage] = [] +#if os(macOS) || os(Linux) +commonPlugins.append( + .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins") +) +#endif + var swiftSettings: [SwiftSetting] = [ .define("MACOS", .when(platforms: [.macOS])), .define("WINDOWS", .when(platforms: [.windows])), @@ -90,7 +100,10 @@ var swiftSettings: [SwiftSetting] = [ .define("ANDROID", .when(platforms: [.android])), .define("LINUX", .when(platforms: [.linux])), .define("DARWIN", .when(platforms: applePlatforms)), - .define("WASM", .when(platforms: [.wasi])) + .define("WASM", .when(platforms: [.wasi])), + .define("ENABLE_DEBUG_DYLIB", .when(configuration: .debug)), + .enableUpcomingFeature("MemberImportVisibility"), + .strictMemorySafety(), ] if isVulkanEnabled { @@ -153,12 +166,9 @@ var adaEngineDependencies: [Target.Dependency] = [ adaEngineDependencies += ["X11"] #endif -let adaEngineTarget: Target = .target( +let adaEngineTarget: Target = .adaTarget( name: "AdaEngine", dependencies: adaEngineDependencies, - exclude: [ - "BUILD.bazel" - ], resources: [ .copy("Assets/Images"), .copy("Assets/Shaders") @@ -170,14 +180,11 @@ let adaEngineTarget: Target = .target( plugins: commonPlugins ) -let adaEngineEmbeddable: Target = .target( +let adaEngineEmbeddable: Target = .adaTarget( name: "AdaEngineEmbeddable", dependencies: [ "AdaEngine", "AdaEngineMacros" - ], - exclude: [ - "BUILD.bazel" ] ) @@ -199,13 +206,8 @@ var targets: [Target] = [ adaEngineTarget, adaEngineEmbeddable, adaEngineMacros, - .target( - name: "Math", - exclude: [ - "BUILD.bazel" - ] - ), - .target( + .adaTarget(name: "Math"), + .adaTarget( name: "AdaApp", dependencies: [ .product(name: "Logging", package: "swift-log"), @@ -213,12 +215,9 @@ var targets: [Target] = [ "AdaECS", "Yams" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaPlatform", dependencies: [ .product(name: "Logging", package: "swift-log"), @@ -227,37 +226,31 @@ var targets: [Target] = [ "AdaApp", "AdaUI" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaECS", dependencies: [ .product(name: "Collections", package: "swift-collections"), .product(name: "BitCollections", package: "swift-collections"), + .product(name: "Atomics", package: "swift-atomics"), + .product(name: "Logging", package: "swift-log"), "AdaEngineMacros", "AdaUtils" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaUtils", dependencies: [ .product(name: "Collections", package: "swift-collections"), .product(name: "BitCollections", package: "swift-collections"), "AdaEngineMacros", - ], - exclude: [ - "BUILD.bazel" + "Math" ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaAssets", dependencies: [ .product(name: "Logging", package: "swift-log"), @@ -265,12 +258,9 @@ var targets: [Target] = [ "AdaUtils", "Yams" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaAudio", dependencies: [ "AdaApp", @@ -281,28 +271,23 @@ var targets: [Target] = [ "miniaudio", "Math" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaTransform", dependencies: [ "AdaApp", "AdaECS", "Math" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaRender", dependencies: [ "AdaApp", "AdaECS", + "AdaAssets", "AdaTransform", "Math", "Yams", @@ -310,15 +295,12 @@ var targets: [Target] = [ "SPIRVCompiler", "libpng", ], - exclude: [ - "BUILD.bazel" - ], resources: [ .copy("Assets/Shaders") ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaText", dependencies: [ "AdaApp", @@ -328,15 +310,12 @@ var targets: [Target] = [ "AdaRender", "AtlasFontGenerator", ], - exclude: [ - "BUILD.bazel" - ], resources: [ .copy("Assets") ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaUI", dependencies: [ "AdaApp", @@ -348,24 +327,18 @@ var targets: [Target] = [ "AdaInput", "AdaEngineMacros", ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaInput", dependencies: [ "AdaApp", "AdaECS", "AdaTransform", ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaScene", dependencies: [ "AdaApp", @@ -378,12 +351,9 @@ var targets: [Target] = [ "AdaUI", "AdaPhysics" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaTilemap", dependencies: [ "AdaApp", @@ -393,12 +363,9 @@ var targets: [Target] = [ "AdaPhysics", "AdaSprite" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaPhysics", dependencies: [ "AdaApp", @@ -408,23 +375,18 @@ var targets: [Target] = [ "box2d", "AdaRender" ], - exclude: [ - "BUILD.bazel" - ], swiftSettings: swiftSettings ), - .target( + .adaTarget( name: "AdaSprite", dependencies: [ "AdaApp", "AdaAssets", "AdaECS", + "AdaText", "Math", "AdaRender" ], - exclude: [ - "BUILD.bazel" - ], resources: [ .copy("Assets") ], @@ -448,14 +410,14 @@ targets += [ // MARK: - CXX Internal Targets targets += [ - .target( + .adaTarget( name: "AtlasFontGenerator", dependencies: [ .product(name: "MSDFAtlasGen", package: "msdf-atlas-gen") ], publicHeadersPath: "include" ), - .target( + .adaTarget( name: "SPIRVCompiler", dependencies: [ "glslang" @@ -538,6 +500,15 @@ targets += [ exclude: [ "BUILD.bazel" ] + ), + .testTarget( + name: "AdaUtilsTests", + dependencies: [ + "AdaUtils" + ], + exclude: [ + "BUILD.bazel" + ] ) ] @@ -551,10 +522,13 @@ let package = Package( name: "AdaEngine", defaultLocalization: "en", platforms: [ - .iOS(.v16), - .macOS(.v14), + .iOS(.v17), + .tvOS(.v17), + .visionOS(.v2), + .macOS(.v15), ], products: products, + traits: [], dependencies: [], targets: targets, cLanguageStandard: .c17, @@ -566,9 +540,11 @@ package.dependencies += [ .package(url: "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/SpectralDragon/Yams.git", revision: "fb676da"), .package(url: "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/apple/swift-log", from: "1.5.4"), .package(url: "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/apple/swift-numerics", from: "1.0.0"), + .package(url: "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/apple/swift-atomics", from: "1.3.0"), // Plugins .package(url: "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/apple/swift-docc-plugin", from: "1.3.0"), .package(url: "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/swiftlang/swift-syntax", from: "600.0.1"), + .package(url: "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/SimplyDanny/SwiftLintPlugins", from: "0.62.1"), .package(path: "Modules/box2d"), .package(path: "Modules/msdf-atlas-gen"), @@ -584,12 +560,9 @@ package.dependencies += [ if isVulkanEnabled { adaEngineTarget.dependencies.append(.target(name: "Vulkan")) package.targets += [ - .target( + .adaTarget( name: "Vulkan", dependencies: ["CVulkan"], - exclude: [ - "BUILD.bazel" - ], cSettings: [ // Apple .define("VK_USE_PLATFORM_IOS_MVK", .when(platforms: [.iOS])), @@ -612,3 +585,105 @@ if isVulkanEnabled { ) ] } + +private extension Target { + /// Creates a regular target. + /// + /// A target can contain either Swift or C-family source files, but not both. It contains code that is built as + /// a regular module for inclusion in a library or executable product, but that cannot itself be used as + /// the main target of an executable product. + /// + /// - Parameters: + /// - name: The name of the target. + /// - dependencies: The dependencies of the target. A dependency can be another target in the package or a product from a package dependency. + /// - path: The custom path for the target. By default, the Swift Package Manager requires a target's sources to reside at predefined search paths; + /// for example, `[PackageRoot]/Sources/[TargetName]`. + /// Don't escape the package root; for example, values like `../Foo` or `/Foo` are invalid. + /// - exclude: A list of paths to files or directories that the Swift Package Manager shouldn't consider to be source or resource files. + /// A path is relative to the target's directory. + /// This parameter has precedence over the ``sources`` parameter. + /// - sources: An explicit list of source files. If you provide a path to a directory, + /// Swift Package Manager searches for valid source files recursively. + /// - resources: An explicit list of resources files. + /// - publicHeadersPath: The directory that contains public headers of a C-family library target. + /// - packageAccess: Allows package symbols from other targets in the package. + /// - cSettings: The C settings for this target. + /// - cxxSettings: The C++ settings for this target. + /// - swiftSettings: The Swift settings for this target. + /// - linkerSettings: The linker settings for this target. + /// - plugins: The plug-ins used by this target + static func adaTarget( + name: String, + dependencies: [Dependency] = [], + path: String? = nil, + exclude: [String] = [], + sources: [String]? = nil, + resources: [Resource]? = nil, + publicHeadersPath: String? = nil, + packageAccess: Bool = true, + cSettings: [CSetting]? = nil, + cxxSettings: [CXXSetting]? = nil, + swiftSettings: [SwiftSetting]? = nil, + linkerSettings: [LinkerSetting]? = nil, + plugins: [PluginUsage]? = nil + ) -> Target { + .target( + name: name, + dependencies: dependencies, + path: path, + exclude: ["BUILD.bazel"] + exclude, + sources: sources, + resources: resources, + publicHeadersPath: publicHeadersPath, + packageAccess: packageAccess, + cSettings: cSettings, + cxxSettings: cxxSettings, + swiftSettings: swiftSettings, + linkerSettings: linkerSettings, + plugins: plugins + ) + } + + static func exampleTarget( + name: String, + path: String, + ) -> Target { + .executableTarget( + name: name, + dependencies: [ + "AdaEngine" + ], + path: "Assets/Examples/\(path)/\(name)", + resources: [ + .copy("../../Resources/") + ] + ) + } +} + +// MARK: - Examples + +let examplesTargets: [Target] = [ + // MARK: 2d + .exampleTarget(name: "BunniesStress", path: "2d"), + .exampleTarget(name: "TransformEntChildren", path: "2d"), + .exampleTarget(name: "CustomMaterial", path: "2d"), + + // MARK: Input + .exampleTarget(name: "GamepadExampleScene", path: "Input"), + + // MARK: Scene + .exampleTarget(name: "scene_load", path: "Scene"), + .exampleTarget(name: "LdtkTilemap", path: "Scene"), + + // MARK: Games + .exampleTarget(name: "SnowmanAttacks", path: "Games"), + + // MARK: UI + .exampleTarget(name: "UITestScene", path: "UI"), + .exampleTarget(name: "AnimatedTextRenderer", path: "UI") +] + +package.targets.append(contentsOf: examplesTargets) + +// MARK: Examples - diff --git a/README.md b/README.md index fff41c095..d95b0149d 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ AdaEngine is a game engine fully written on Swift. The main idea is to encourage * **[AdaEngine Awesome Projects](https://github.com/AdaEngine/AdaEngineAwesome)**: AdaEngine's offical Awesome Projects page. Feel free to explore. +* **[AdaEngine Examples](https://github.com/AdaEngine/AdaEngine/tree/main/Assets/Examples/)**: AdaEngine's internal examples. + ## Getting started We recommend checking out the **[Create your first project guide](https://adaengine.github.io/adaengine-docs/tutorials/adaengine/createproject)** for a brief introduction. diff --git a/Sources/AdaApp/App.swift b/Sources/AdaApp/App.swift index 417bd490b..53b987c61 100644 --- a/Sources/AdaApp/App.swift +++ b/Sources/AdaApp/App.swift @@ -28,6 +28,6 @@ public extension App { // Initializes and runs the app. static func main() async throws { let appContext = try AppContext() - try appContext.run() + try await appContext.run() } } diff --git a/Sources/AdaApp/AppBuilder.swift b/Sources/AdaApp/AppBuilder.swift index 27550866a..4fd4054e4 100644 --- a/Sources/AdaApp/AppBuilder.swift +++ b/Sources/AdaApp/AppBuilder.swift @@ -8,6 +8,8 @@ import AdaECS import AdaUtils +// TODO: Do we need be a Main Actor??? + /// A protocol that represents a world extractor. /// Used to extract data from main world to subworlds. public protocol WorldExctractor { @@ -15,20 +17,20 @@ public protocol WorldExctractor { /// - Parameters: /// - mainWorld: The main world. /// - world: The subworld. - func exctract(from mainWorld: World, to world: World) + func exctract(from mainWorld: World, to world: World) async } /// A class that represents a collection of worlds. @MainActor public final class AppWorlds { /// The main world. - public var mainWorld: World + public var main: World /// The subworlds. var subWorlds: [String: AppWorlds] /// The world extractor. - var worldExctractor: (any WorldExctractor)? + nonisolated(unsafe) var worldExctractor: (any WorldExctractor)? /// The plugins. var plugins: [ObjectIdentifier: any Plugin] = [:] @@ -39,19 +41,19 @@ public final class AppWorlds { /// The flag that indicates if the app is configured. var isConfigured: Bool = false - var scedulers: [Scheduler] + /// Default scheduler that will run first in ``update()`` method + public var updateScheduler: SchedulerName? /// Initialize a new instance of `AppWorlds` with the given main world and subworlds. /// - Parameters: /// - mainWorld: The main world. /// - subWorlds: The subworlds. - init( - mainWorld: World, + public init( + main: World, subWorlds: [String : AppWorlds] = [:] ) { - self.mainWorld = mainWorld + self.main = main self.subWorlds = subWorlds - self.scedulers = [Scheduler(name: .update)] } } @@ -60,13 +62,7 @@ public extension AppWorlds { /// Set the world extractor. /// - Parameter exctractor: The world extractor. func setExctractor(_ exctractor: any WorldExctractor) { - self.worldExctractor = exctractor - } - - /// Set the world scheduler - /// - Parameter scheduler: The world scheduler. - func setSchedulers(_ schedulers: [Scheduler]) { - self.scedulers = schedulers + unsafe self.worldExctractor = exctractor } /// Set the runner. @@ -81,17 +77,18 @@ public extension AppWorlds { if !isConfigured { return } - - for sceduler in self.scedulers { - await sceduler.run(world: mainWorld) - - for world in self.subWorlds.values { - world.worldExctractor?.exctract(from: mainWorld, to: world.mainWorld) - await world.update() - } + guard let updateScheduler else { + assertionFailure("Update scheduler is empty") + return } + await main.runScheduler(updateScheduler) - mainWorld.clearTrackers() + for world in self.subWorlds.values { + unsafe await world.worldExctractor?.exctract(from: main, to: world.main) + await world.update() + } + + main.clearTrackers() } /// Get the subworld builder by name. @@ -101,13 +98,11 @@ public extension AppWorlds { self.subWorlds[name.rawValue] } - /// Create a new subworld. + /// Add a new subworld. + /// - Parameter subworld: The subworld. /// - Parameter name: The name of the subworld. - /// - Returns: The subworld builder. - func createSubworld(by name: AppWorldName) -> AppWorlds { - let subworld = AppWorlds(mainWorld: World(name: name.rawValue)) + func addSubworld(_ subworld: consuming AppWorlds, by name: AppWorldName) { self.subWorlds[name.rawValue] = subworld - return subworld } /// Add a plugin to the app. @@ -116,7 +111,8 @@ public extension AppWorlds { @discardableResult func addPlugin(_ plugin: T) -> Self { if self.plugins[ObjectIdentifier(T.self)] != nil { - fatalError("Plugin already installed") + assertionFailure("Plugin already installed") + return self } self.plugins[ObjectIdentifier(T.self)] = plugin @@ -134,25 +130,22 @@ public extension AppWorlds { _ system: T.Type, on scheduler: AdaECS.SchedulerName = .update ) -> Self { - self.mainWorld.addSystem(system, on: scheduler) + self.main.addSystem(system, on: scheduler) return self } - /// Add an entity to the main world. - /// - Parameter entity: The entity to add. + /// Insert a resource to the world. + /// - Parameter resource: The resource to insert. /// - Returns: The app builder. @discardableResult - func addEntity(_ entity: Entity) -> Self { - self.mainWorld.addEntity(entity) + func insertResource(_ resource: consuming T) -> Self { + self.main.insertResource(resource) return self } - /// Insert a resource to the world. - /// - Parameter resource: The resource to insert. - /// - Returns: The app builder. @discardableResult - func insertResource(_ resource: consuming T) -> Self { - self.mainWorld.insertResource(resource) + func createResource(_ type: T.Type) -> Self { + _ = self.main.createResource(of: type) return self } @@ -160,52 +153,60 @@ public extension AppWorlds { /// - Parameter resource: The resource to insert. /// - Returns: The app builder. func getResource(_ resource: T.Type) -> T? { - return self.mainWorld.getResource(resource) + return self.main.getResource(resource) } - func build() throws { + /// Get mutable resource from the world. + /// - Parameter resource: The resource to insert. + /// - Returns: The app builder. + func getRefResource(_ resource: T.Type) -> Ref { + self.main.getRefResource(resource) + } + + func build() async throws { /// Wait until all plugins is loaded - while !self.plugins.allSatisfy({ $0.value.isLoaded() }) { - continue + while !self.plugins.allSatisfy({ $0.value.isLoaded(in: self) }) { + await Task.yield() } - - self.mainWorld.build() - try self.subWorlds.values.forEach { - try $0.build() + + for subWorld in self.subWorlds.values { + try await subWorld.build() } + self.plugins.forEach { $0.value.finish(for: self) } self.isConfigured = true } } +// TODO: Maybe setup made async??? /// A protocol that represents a plugin for the app. public protocol Plugin: Sendable { /// Setup the plugin in the app. @MainActor - func setup(in app: AppWorlds) + func setup(in app: borrowing AppWorlds) /// Notify the plugin that the app is ready. @MainActor - func finish() + func finish(for app: borrowing AppWorlds) /// Check if the plugin is loaded. Used for async plugins. @MainActor - func isLoaded() -> Bool + func isLoaded(in app: borrowing AppWorlds) -> Bool /// Destroy the plugin. @MainActor - func destroy() + func destroy(for app: borrowing AppWorlds) } public extension Plugin { - func isLoaded() -> Bool { + func isLoaded(in app: borrowing AppWorlds) -> Bool { return true } - func finish() { } + func finish(for app: borrowing AppWorlds) { } @MainActor - func destroy() { } + func destroy(for app: borrowing AppWorlds) { } } public struct AppWorldName: Hashable, Equatable, RawRepresentable, CustomStringConvertible, Sendable { diff --git a/Sources/AdaApp/AppContext.swift b/Sources/AdaApp/AppContext.swift index 0f2b963e8..a0aef7140 100644 --- a/Sources/AdaApp/AppContext.swift +++ b/Sources/AdaApp/AppContext.swift @@ -12,7 +12,7 @@ import Logging /// The context of the app. @MainActor @_spi(Internal) -public final class AppContext { +public struct AppContext: ~Copyable { /// The app. private let app: T @@ -32,11 +32,11 @@ public final class AppContext { /// Run the app. /// - Throws: An error if the app cannot be run. @_spi(Internal) - public func run() throws { + public func run() async throws { LoggingSystem.bootstrap { StreamLogHandler.standardError(label: $0) } - let appWorlds = AppWorlds(mainWorld: World(name: "MainWorld")) + let appWorlds = AppWorlds(main: World(name: "MainWorld")) appWorlds .insertResource(WindowSettings()) .addPlugin(MainSchedulerPlugin()) @@ -44,7 +44,8 @@ public final class AppContext { let inputs = _SceneInputs(appWorlds: appWorlds) let node = _AppSceneNode(value: app.body) let _ = T.Content._makeView(node, inputs: inputs) - try appWorlds.build() + + try await appWorlds.build() appWorlds.runner?(appWorlds) } } diff --git a/Sources/AdaApp/AppScenes/AppScene.swift b/Sources/AdaApp/AppScenes/AppScene.swift index fdf782de6..b864b912d 100644 --- a/Sources/AdaApp/AppScenes/AppScene.swift +++ b/Sources/AdaApp/AppScenes/AppScene.swift @@ -6,7 +6,11 @@ // import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Math /// Describe which kind of scene will present on start. @@ -152,9 +156,9 @@ public struct _AppSceneNode: Equatable { // if its pod, we can compare it together using memcmp. if _isPOD(Value.self) { let memSize = MemoryLayout.size - return withUnsafePointer(to: lhs.value) { lhsPtr in - withUnsafePointer(to: rhs.value) { rhsPtr in - memcmp(lhsPtr, rhsPtr, memSize) == 0 + return unsafe withUnsafePointer(to: lhs.value) { lhsPtr in + unsafe withUnsafePointer(to: rhs.value) { rhsPtr in + unsafe memcmp(lhsPtr, rhsPtr, memSize) == 0 } } } else { diff --git a/Sources/AdaApp/AppScenes/Never+AppScene.swift b/Sources/AdaApp/AppScenes/Never+AppScene.swift index 7049f26da..e58852675 100644 --- a/Sources/AdaApp/AppScenes/Never+AppScene.swift +++ b/Sources/AdaApp/AppScenes/Never+AppScene.swift @@ -6,5 +6,14 @@ // extension Never: AppScene { - public var scene: Never { fatalError() } + public var body: Never { fatalError("Never has no body") } + + @MainActor @preconcurrency + public static func _makeView( + _ scene: _AppSceneNode, + inputs: _SceneInputs + ) -> _SceneOutputs { + // For Never, just forward inputs as outputs as per default behavior. + _SceneOutputs(appWorlds: inputs.appWorlds) + } } diff --git a/Sources/AdaApp/AppScenes/SceneModifiers/DefaultSceneModifiers.swift b/Sources/AdaApp/AppScenes/SceneModifiers/DefaultSceneModifiers.swift index 142e158d8..35dc5616d 100644 --- a/Sources/AdaApp/AppScenes/SceneModifiers/DefaultSceneModifiers.swift +++ b/Sources/AdaApp/AppScenes/SceneModifiers/DefaultSceneModifiers.swift @@ -96,9 +96,8 @@ public extension AppScene { value: Value ) -> some AppScene { transformAppWorlds { worlds in - guard var resource = worlds.getResource(T.self) else { return } - resource[keyPath: keyPath] = value - worlds.insertResource(resource) + let resource = worlds.main.getRefResource(T.self) + resource.wrappedValue[keyPath: keyPath] = value } } } diff --git a/Sources/AdaApp/MainScheduler.swift b/Sources/AdaApp/MainScheduler.swift index b9a4f243d..fc269845d 100644 --- a/Sources/AdaApp/MainScheduler.swift +++ b/Sources/AdaApp/MainScheduler.swift @@ -13,28 +13,43 @@ struct MainSchedulerPlugin: Plugin { /// Setup the main scheduler. /// - Parameter app: The app to setup the main scheduler for. func setup(in app: AppWorlds) { - let mainScheduler = Scheduler(name: .update) - let fixedScheduler = Scheduler(name: .fixedUpdate, system: FixedTimeSchedulerSystem.self) - let postUpdateScheduler = Scheduler(name: .postUpdate, system: PostUpdateSchedulerRunner.self) - app.setSchedulers([ - mainScheduler, - fixedScheduler, - postUpdateScheduler - ]) - app.mainWorld.insertResource(DefaultSchedulerOrder()) - app.mainWorld.addSchedulers( - .fixedPreUpdate, - .fixedUpdate, - .fixedPostUpdate + let mainScheduler = Scheduler(name: .main) + app.updateScheduler = .main + + app.main.addScheduler(mainScheduler) + app.main.addSchedulers( + .fixed, + .postUpdate ) + + app + .addSystem(DefaultSchedulerRunner.self, on: .main) + .addSystem(FixedTimeSchedulerSystem.self, on: .fixed) + app.insertResource(DefaultSchedulerOrder()) + app.addSystem(GameLoopBeganSystem.self, on: .preUpdate) } } -/// The system that runs the fixed time scheduler. +extension SchedulerName { + static let main: SchedulerName = "Main" + static let fixed: SchedulerName = "FixedMain" +} + +// FIXME: Hack to works with AnimatedTexture @System +@inline(__always) +func GameLoopBegan( + _ deltaTime: Res +) { + guard let deltaTime = deltaTime.wrappedValue else { return } + EventManager.default.send(EngineEvents.MainLoopBegan(deltaTime: deltaTime.deltaTime)) +} + +/// The system that runs the fixed time scheduler. +@PlainSystem public struct FixedTimeSchedulerSystem { - @LocalIsolated + @Local private var fixedTimestep: FixedTimestep let order: [SchedulerName] = [ @@ -47,43 +62,24 @@ public struct FixedTimeSchedulerSystem { self.fixedTimestep = FixedTimestep(stepsPerSecond: 60) } - public func update(context: inout UpdateContext) { - let result = self.fixedTimestep.advance(with: context.deltaTime) + @Res + private var deltaTime + + public func update(context: UpdateContext) async { + let deltaTime = deltaTime?.deltaTime ?? 0 + let result = self.fixedTimestep.advance(with: deltaTime) if result.isFixedTick { let step = self.fixedTimestep.step let world = context.world - world.insertResource(DeltaTime(deltaTime: step)) - context.taskGroup.addTask { [order] in - for scheduler in order { - await world.runScheduler(scheduler, deltaTime: step) - } + world.insertResource(FixedTime(deltaTime: step)) + for scheduler in order { + await world.runScheduler(scheduler) } } } } -/// The system that runs the post update scheduler. -@System -public struct PostUpdateSchedulerRunner: Sendable { - - @ResQuery - private var order: DefaultSchedulerOrder? - - @LocalIsolated - private var lastUpdate: LongTimeInterval = 0 - - public init(world: World) { } - - public func update(context: inout UpdateContext) { - let world = context.world - let deltaTime = context.deltaTime - context.taskGroup.addTask { - await world.runScheduler(.postUpdate, deltaTime: deltaTime) - } - } -} - extension SchedulerName { /// The fixed pre-update scheduler. public static let fixedPreUpdate = SchedulerName(rawValue: "fixedPreUpdate") diff --git a/Sources/AdaAssets/Asset.swift b/Sources/AdaAssets/Asset.swift index a7cabc8be..565c7f958 100644 --- a/Sources/AdaAssets/Asset.swift +++ b/Sources/AdaAssets/Asset.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 11/10/21. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// The interface describe asset in a system. /// Asset describe information needed to your game, like Audio, Mesh, Texture and etc. @@ -32,18 +36,18 @@ import Foundation /// /// Also, your asset can support ``Codable`` behaviour and for this scenario, you should implement only ``init(from decoder: Decoder)`` and ``func encode(to encoder: Encoder)`` methods. /// Meta and other information will be available from userInfo. Use `Decoder.assetsDecodingContext`, `Decoder.assetMeta` and `Encoder.assetMeta` properties to get this info. -public protocol Asset: AnyObject, Sendable { +public protocol Asset: Sendable { /// When asset load from the disk, this method will be called. /// /// - Parameter data: Asset's data. /// - Returns: Return instance of asset - init(from assetDecoder: AssetDecoder) throws + init(from assetDecoder: AssetDecoder) async throws /// To store asset on the disk, you should implement this method. /// /// - Returns: the asset data to be saved - func encodeContents(with assetEncoder: AssetEncoder) throws + func encodeContents(with assetEncoder: AssetEncoder) async throws /// Extensions for asset. static func extensions() -> [String] @@ -133,15 +137,22 @@ public struct AssetMetaInfo: Codable, Sendable { /// assetHandle.update(newAsset) /// ``` /// -public final class AssetHandle: Codable, Sendable { +public final class AssetHandle: Codable, @unchecked Sendable { /// The asset instance. - public private(set) nonisolated(unsafe) var asset: T - + public private(set) var asset: T! + + public var isLoaded: Bool { + self.asset != nil + } + /// Initialize a new asset handle from an asset. /// - Parameter asset: The asset to initialize the asset handle from. /// - Warning: Only assets produced by ``AssetsManager`` can be used in hot reloading. public init(_ asset: T) { self.asset = asset + self.type = String(reflecting: Swift.type(of: self.asset)) + self.assetMeta = asset.assetMetaInfo + self.assetPath = asset.assetPath } enum CodingKeys: CodingKey { @@ -149,32 +160,47 @@ public final class AssetHandle: Codable, Sendable { case assetPath case meta } - + + private let type: String + private let assetPath: String + private let assetMeta: AssetMetaInfo? + /// Initialize a new asset handle from a decoder. /// /// - Parameter decoder: The decoder to initialize the asset handle from. /// - Throws: An error if the asset handle cannot be initialized from the decoder. public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(String.self, forKey: .type) - let assetType = AssetsManager.getAssetType(for: type) ?? T.self - let superDecoder = try container.superDecoder(forKey: .meta) - let asset = try decoder.assetsDecoder.decode(assetType, from: superDecoder) - self.asset = asset as! T + self.type = try container.decode(String.self, forKey: .type) + self.assetPath = try container.decode(String.self, forKey: .assetPath) + self.assetMeta = try container.decode(AssetMetaInfo.self, forKey: .meta) } - + + public func load() async throws { + let asset = if let assetMeta, let path = assetMeta.bundlePath, let bundle = Bundle(path: path) { + try await AssetsManager.load( + T.self, + at: assetPath, + from: bundle + ) + } else { + try await AssetsManager.load( + T.self, + at: assetPath + ) + } + self.asset = asset.asset + } + /// Encode the asset handle to an encoder. /// /// - Parameter encoder: The encoder to encode the asset handle to. /// - Throws: An error if the asset handle cannot be encoded to the encoder. public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(String(reflecting: type(of: self.asset)), forKey: .type) - if !asset.assetPath.isEmpty { - try container.encode(asset.assetPath, forKey: .assetPath) - } - let superEncoder = container.superEncoder(forKey: .meta) - try encoder.assetsEncoder.encode(asset, to: superEncoder) + try container.encode(type, forKey: .type) + try container.encode(assetPath, forKey: .assetPath) + try container.encode(assetMeta, forKey: .meta) } func update(_ newAsset: T) async throws { @@ -189,7 +215,7 @@ extension AssetHandle: AnyAssetHandle { throw AssetError.message("Asset \(newAsset) is not of type \(T.self)") } - self.asset = newAsset as! T + self.asset = newAsset as? T } } diff --git a/Sources/AdaAssets/AssetsCodable/AssetsCodable.swift b/Sources/AdaAssets/AssetsCodable/AssetsCodable.swift index cb099e00c..1626dda1d 100644 --- a/Sources/AdaAssets/AssetsCodable/AssetsCodable.swift +++ b/Sources/AdaAssets/AssetsCodable/AssetsCodable.swift @@ -5,13 +5,17 @@ // Created by v.prusakov on 3/9/23. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import AdaUtils // TODO: Mode for decoding/encoding files from/into binary format. /// A query for an asset. -public struct AssetQuery: Sendable { +public struct AssetQuery: Sendable, Codable { /// The name of the query. public let name: String @@ -20,7 +24,7 @@ public struct AssetQuery: Sendable { } /// A meta information about an asset. -public struct AssetMeta: Sendable { +public struct AssetMeta: Sendable, Codable { /// The file path of the asset. public let filePath: URL @@ -64,7 +68,7 @@ public protocol AssetEncoder: Sendable { /// - Note: If you call this method more than once, than previous encode data will overwritten. func encode(_ value: T) throws - func encode(_ asset: A, to encoder: any Encoder) throws + func encode(_ asset: A, to encoder: any Encoder) async throws } // MARK: - Decoder - @@ -96,13 +100,13 @@ public protocol AssetDecoder: Sendable { /// - Parameter type: The type of the content. /// - Returns: The decoded content. func decode(_ type: T.Type) throws -> T - + /// Use this method to decode content from asset. /// /// - Parameter type: The type of the asset. /// - Parameter decoder: The decoder. /// - Returns: The decoded asset. - func decode(_ type: A.Type, from decoder: any Decoder) throws -> A + func decode(_ type: A.Type, from decoder: any Decoder) async throws -> A } // MARK: Asset Decoding Context diff --git a/Sources/AdaAssets/AssetsCodable/TextAssetDecoder.swift b/Sources/AdaAssets/AssetsCodable/TextAssetDecoder.swift index 8249d4232..88a6cbf68 100644 --- a/Sources/AdaAssets/AssetsCodable/TextAssetDecoder.swift +++ b/Sources/AdaAssets/AssetsCodable/TextAssetDecoder.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 5/2/24. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import AdaUtils import Yams @@ -37,14 +41,14 @@ public final class TextAssetDecoder: AssetDecoder, @unchecked Sendable { /// - type: The type of the asset. /// - decoder: The decoder to decode the asset from. /// - Returns: The decoded asset. - public func decode(_ type: A.Type, from decoder: any Decoder) throws -> A { + public func decode(_ type: A.Type, from decoder: any Decoder) async throws -> A { let newDecoder = Self( meta: self.assetMeta, data: self.assetData, decoder: decoder ) - return try A.init(from: newDecoder) + return try await A.init(from: newDecoder) } /// Get or load a resource from the decoder. @@ -61,7 +65,7 @@ public final class TextAssetDecoder: AssetDecoder, @unchecked Sendable { return AssetHandle(value) } else { let handle = try AssetsManager.loadSync(resourceType, at: path) - self.appendResource(handle.asset) + self.appendResource(handle) return handle } @@ -92,8 +96,8 @@ public final class TextAssetDecoder: AssetDecoder, @unchecked Sendable { /// Append a resource to the decoder. /// /// - Parameter resource: The resource to append. - public func appendResource(_ resource: A) { - self.resources[resource.assetPath] = WeakBox(value: resource) + public func appendResource(_ resource: AssetHandle) { + self.resources[resource.asset.assetPath] = WeakBox(value: resource) } } diff --git a/Sources/AdaAssets/AssetsCodable/TextAssetEncoder.swift b/Sources/AdaAssets/AssetsCodable/TextAssetEncoder.swift index a8f9057e2..b202e3001 100644 --- a/Sources/AdaAssets/AssetsCodable/TextAssetEncoder.swift +++ b/Sources/AdaAssets/AssetsCodable/TextAssetEncoder.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 5/2/24. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Yams /// An encoder for assets that are stored in text format. @@ -61,9 +65,9 @@ public final class TextAssetEncoder: AssetEncoder, @unchecked Sendable { /// - asset: The asset to encode. /// - encoder: The encoder to encode the asset to. /// - Throws: An error if the asset cannot be encoded to the encoder. - public func encode(_ asset: A, to encoder: any Encoder) throws { + public func encode(_ asset: A, to encoder: any Encoder) async throws { let newEncoder = Self(meta: self.assetMeta, encoder: encoder) - try asset.encodeContents(with: newEncoder) + try await asset.encodeContents(with: newEncoder) } } diff --git a/Sources/AdaAssets/AssetsManager.swift b/Sources/AdaAssets/AssetsManager.swift index f76266f8a..8dbcff042 100644 --- a/Sources/AdaAssets/AssetsManager.swift +++ b/Sources/AdaAssets/AssetsManager.swift @@ -7,7 +7,11 @@ import AdaECS import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Logging public enum AssetError: LocalizedError { @@ -25,6 +29,7 @@ public enum AssetError: LocalizedError { } // TODO: In the future, we should compile assets into binary +// TODO: Remove unsafe and statics /// Manager using for loading and saving assets in file system. /// Each asset loaded from manager stored in memory cache. @@ -145,7 +150,7 @@ public struct AssetsManager: Resource { public static func load( _ type: A.Type, at path: String, - from bundle: Bundle, + from bundle: Foundation.Bundle, handleChanges: Bool = false ) async throws -> AssetHandle { let key = self.makeCacheKey(resource: A.self, path: path) @@ -186,7 +191,7 @@ public struct AssetsManager: Resource { public static func loadSync( _ type: R.Type, at path: String, - from bundle: Bundle + from bundle: Foundation.Bundle ) throws -> AssetHandle { let task = UnsafeTask> { return try await load(type, at: path, from: bundle) @@ -233,8 +238,8 @@ public struct AssetsManager: Resource { let meta = AssetMeta(filePath: processedPath.url, queryParams: processedPath.query) let defaultEncoder = TextAssetEncoder(meta: meta) - try asset.encodeContents(with: defaultEncoder) - + try await asset.encodeContents(with: defaultEncoder) + let intermediateDirs = processedPath.url.deletingLastPathComponent() if !fileSystem.itemExists(at: intermediateDirs) { @@ -268,12 +273,12 @@ public struct AssetsManager: Resource { // MARK: - Public methods public static func getAssetType(for typeName: String) -> (any Asset.Type)? { - return registredAssetTypes[typeName] + return unsafe registredAssetTypes[typeName] } public static func registerAssetType(_ type: T.Type) { Task { @AssetActor in - registredAssetTypes[String(reflecting: type)] = T.self + unsafe registredAssetTypes[String(reflecting: type)] = T.self } } @@ -284,7 +289,7 @@ public struct AssetsManager: Resource { try FileSystem.current.createDirectory(at: url, withIntermediateDirectories: true) } - self.resourceDirectory = url + unsafe self.resourceDirectory = url self.storage.loadedAssets.removeAll() } @@ -295,10 +300,10 @@ public struct AssetsManager: Resource { @_spi(AdaEngine) public static func initialize(filePath: StaticString) throws { let projectDirectories = try URL.findProjectDirectories(from: filePath) - self.projectDirectories = projectDirectories - + unsafe self.projectDirectories = projectDirectories + #if DEBUG - self.resourceDirectory = projectDirectories.assetsDirectory + unsafe self.resourceDirectory = projectDirectories.assetsDirectory #else let fileSystem = FileSystem.current let resources = projectDirectories.assetsDirectory @@ -365,19 +370,19 @@ public struct AssetsManager: Resource { } let meta = AssetMeta(filePath: path.url, queryParams: path.query) let decoder = TextAssetDecoder(meta: meta, data: data) - try assetType.loadAndUpdateInternal(from: decoder, oldResource: oldResource) + try await assetType.loadAndUpdateInternal(from: decoder, oldResource: oldResource) } @AssetActor - private static func load(from path: Path, originalPath: String, bundle: Bundle?) async throws -> A { + private static func load(from path: Path, originalPath: String, bundle: Foundation.Bundle?) async throws -> A { guard let data = FileSystem.current.readFile(at: path.url) else { throw AssetError.notExistAtPath(path.url.path) } let meta = AssetMeta(filePath: path.url, queryParams: path.query) let decoder = TextAssetDecoder(meta: meta, data: data) - let resource = try A.init(from: decoder) - + var resource = try await A.init(from: decoder) + resource.assetMetaInfo = AssetMetaInfo( assetPath: originalPath, assetName: path.url.lastPathComponent, @@ -424,12 +429,12 @@ private extension AssetsManager { private static func processPath(_ path: String) -> Path { var path = path var url: URL - - if path.hasPrefix(self.resKeyWord) { + + if path.hasPrefix(self.resKeyWord) && !path.hasPrefix("file://") { path.removeFirst(self.resKeyWord.count) - url = self.resourceDirectory.appendingPathComponent(path) + url = unsafe self.resourceDirectory.appendingPathComponent(path) } else { - url = URL(fileURLWithPath: path) + url = path.hasPrefix("file://") ? URL(string: path)! : URL(fileURLWithPath: path) } let splitComponents = url.lastPathComponent.split(separator: "#") @@ -513,7 +518,6 @@ private extension AssetsManager { } catch { self.result = .failure(error) } - semaphore.signal() } } @@ -545,7 +549,7 @@ extension AssetsManager { /// Actor for loading and saving resources. @globalActor public actor AssetActor { - public static var shared = AssetActor() + public static let shared = AssetActor() } private extension Asset { @@ -553,8 +557,8 @@ private extension Asset { static func loadAndUpdateInternal( from asset: any AssetDecoder, oldResource: any AnyAssetHandle - ) throws { - let resource = try Self.init(from: asset) + ) async throws { + let resource = try await Self.init(from: asset) try oldResource.update(resource) } } diff --git a/Sources/AdaAssets/AssetsPlugin.swift b/Sources/AdaAssets/AssetsPlugin.swift index 13db10be2..424c7152e 100644 --- a/Sources/AdaAssets/AssetsPlugin.swift +++ b/Sources/AdaAssets/AssetsPlugin.swift @@ -6,6 +6,7 @@ // import AdaApp +import AdaECS public struct AssetsPlugin: Plugin { @@ -18,8 +19,23 @@ public struct AssetsPlugin: Plugin { public func setup(in app: AppWorlds) { do { try AssetsManager.initialize(filePath: filePath) + app.addSystem(AssetsProcessSystem.self, on: .preUpdate) } catch { print(error) } } } + +@System +@inline(__always) +func AssetsProcess( + _ context: WorldUpdateContext +) { + Task { + do { + try await AssetsManager.processResources() + } catch { + print("Failed to process") + } + } +} diff --git a/Sources/AdaAudio/AudioPlugin.swift b/Sources/AdaAudio/AudioPlugin.swift index 67a9c559d..f636108cc 100644 --- a/Sources/AdaAudio/AudioPlugin.swift +++ b/Sources/AdaAudio/AudioPlugin.swift @@ -12,37 +12,27 @@ import AdaTransform /// A plugin that adds audio capabilities to the world. public struct AudioPlugin: Plugin { - var engine: AudioEngine? + public init() {} - public init() { + public func setup(in app: borrowing AppWorlds) { do { - self.engine = try MiniAudioEngine() - } catch { - print("Error", error) - } - } - - public func setup(in app: AppWorlds) { - guard let engine else { - return - } - do { - try engine.start() + try AudioServer.initialize() + unsafe try AudioServer.shared.engine.start() AudioComponent.registerComponent() AudioReceiver.registerComponent() AudioPlaybacksControllers.registerComponent() - app - .insertResource(engine) + unsafe app + .insertResource(AudioServer.shared!) .addSystem(AudioSystem.self) } catch { print("Error", error) } } - public func finish() { + public func finish(for app: borrowing AppWorlds) { do { - try engine?.stop() + unsafe try AudioServer.shared.stop() } catch { print("Error", error) } @@ -66,7 +56,7 @@ public struct AudioComponent { /// /// - Parameter resource: The audio resource to play. public init(resource: AudioResource) { - self.playbackController = AudioServer.shared.prepareAudio(resource) + self.playbackController = unsafe AudioServer.shared.prepareAudio(resource) } } @@ -93,7 +83,7 @@ public struct AudioReceiver { } /// A system that manages audio resources for spatial audio. -@System +@PlainSystem public struct AudioSystem { @Query @@ -102,13 +92,12 @@ public struct AudioSystem { @Query, Transform> private var audioReceiverQuery - let audioEngine: AudioEngine! + @Res + private var audioServer - public init(world: World) { - self.audioEngine = world.getResource(MiniAudioEngine.self)! - } + public init(world: World) { } - public func update(context: inout UpdateContext) { + public func update(context: UpdateContext) { self.audioPlaybacksControllersQuery.forEach { audioComponent, transform in audioComponent.controllers.forEach { controller in controller.sound.position = transform.position @@ -119,7 +108,7 @@ public struct AudioSystem { if let listener = audioReceiver.audioListener, listener.position != transform.position { listener.position = transform.position } else { - audioReceiver.audioListener = self.audioEngine.getAudioListener(at: 0) + audioReceiver.audioListener = audioServer.engine.getAudioListener(at: 0) } } } @@ -140,13 +129,14 @@ public extension Entity { /// - Note: Audio controller will be automatically freed when entity is removed from memory and nobody own a reference to the playback controller. /// /// When you create an audio playback controller engine will automatically update position for spatial audio. + @MainActor func prepareAudio(_ resource: AudioResource) -> AudioPlaybackController { var controllers = self.components[AudioPlaybacksControllers.self] ?? AudioPlaybacksControllers() if let controller = controllers.controllers.first(where: { $0.resource === resource }) { return controller } - var playbackController = AudioServer.shared.prepareAudio(resource) + var playbackController = unsafe AudioServer.shared.prepareAudio(resource) playbackController.entity = self controllers.controllers.append(playbackController) self.components += controllers @@ -160,6 +150,7 @@ public extension Entity { /// Use the controller to set playback characteristics like volume and reverb, and then start or stop playback. /// /// This method first prepares the audio by calling ``Entity/prepareAudio(_:)``, and then immediately calls the ``AudioPlaybackController/play()`` method on the returned controller. + @MainActor @discardableResult func playAudio(_ resource: AudioResource) -> AudioPlaybackController { let controller = self.prepareAudio(resource) @@ -172,6 +163,7 @@ public extension Entity { /// You can stop a specific ``AudioPlaybackController`` instance from playing a particular resource /// by calling the controller’s ``AudioPlaybackController/stop()`` method. /// To stop all controllers associated with a particular Entity instance with a single call, use the ``Entity/stopAllAudio()`` method instead. + @MainActor func stopAllAudio() { self.components[AudioPlaybacksControllers.self]?.controllers.forEach { $0.stop() } self.components += AudioPlaybacksControllers() diff --git a/Sources/AdaAudio/AudioResource.swift b/Sources/AdaAudio/AudioResource.swift index 73c6ed480..675a7eaf5 100644 --- a/Sources/AdaAudio/AudioResource.swift +++ b/Sources/AdaAudio/AudioResource.swift @@ -7,7 +7,11 @@ import AdaAssets import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// An audio resource that can be played. /// The AudioResource class stores audio data that you can play in your scene or entire app. @@ -17,11 +21,11 @@ public final class AudioResource: Asset, @unchecked Sendable { private var sound: Sound - public required init(from decoder: AssetDecoder) throws { + public required init(from decoder: AssetDecoder) async throws { if Self.extensions().contains(where: { decoder.assetMeta.filePath.pathExtension == $0 }) { - self.sound = try AudioServer.shared.engine.makeSound(from: decoder.assetData) + self.sound = unsafe try AudioServer.shared.engine.makeSound(from: decoder.assetData) } else { - self.sound = try AudioServer.shared.engine.makeSound(from: decoder.assetMeta.filePath) + self.sound = unsafe try AudioServer.shared.engine.makeSound(from: decoder.assetMeta.filePath) } } @@ -48,13 +52,13 @@ public final class AudioResource: Asset, @unchecked Sendable { /// Create a new instance of audio resource from a file. /// - Note: Supports `WAV` format. public static func create(from url: URL) throws -> AudioResource { - let sound = try AudioServer.shared.engine.makeSound(from: url) + let sound = unsafe try AudioServer.shared.engine.makeSound(from: url) return AudioResource(sound: sound) } /// Create a new instance of audio resource from a data. public static func create(from data: Data) throws -> AudioResource { - let sound = try AudioServer.shared.engine.makeSound(from: data) + let sound = unsafe try AudioServer.shared.engine.makeSound(from: data) return AudioResource(sound: sound) } } diff --git a/Sources/AdaAudio/AudioServer.swift b/Sources/AdaAudio/AudioServer.swift index 9c8aa0ecf..48dc581b6 100644 --- a/Sources/AdaAudio/AudioServer.swift +++ b/Sources/AdaAudio/AudioServer.swift @@ -7,6 +7,11 @@ import AdaECS import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif import Math enum AudioError: Error { @@ -29,7 +34,7 @@ public final class AudioServer: Resource { @MainActor public static func initialize() throws { let engine = try MiniAudioEngine() - self.shared = AudioServer(engine: engine) + unsafe self.shared = AudioServer(engine: engine) } func update(_ deltaTime: AdaUtils.TimeInterval) { diff --git a/Sources/AdaAudio/Engines/AudioEngine.swift b/Sources/AdaAudio/Engines/AudioEngine.swift index b1aeb5e27..baf193708 100644 --- a/Sources/AdaAudio/Engines/AudioEngine.swift +++ b/Sources/AdaAudio/Engines/AudioEngine.swift @@ -7,7 +7,11 @@ import AdaECS import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// Interface that works with audio. protocol AudioEngine: Resource { diff --git a/Sources/AdaAudio/Engines/MiniAudioEngine.swift b/Sources/AdaAudio/Engines/MiniAudioEngine.swift index 5a14d1254..68c0f995c 100644 --- a/Sources/AdaAudio/Engines/MiniAudioEngine.swift +++ b/Sources/AdaAudio/Engines/MiniAudioEngine.swift @@ -5,8 +5,13 @@ // Created by v.prusakov on 5/6/23. // +import AdaECS import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import miniaudio import Math @@ -21,35 +26,50 @@ enum MAError: LocalizedError { } } -final class MiniAudioEngine: AudioEngine, @unchecked Sendable { - private var engine: UnsafeMutablePointer = .allocate( - capacity: MemoryLayout.size(ofValue: ma_engine.self) - ) +@safe +struct MiniAudioEngine: AudioEngine, @unchecked Sendable { - init() throws { - var config = ma_engine_config_init() - config.channels = 2 - let result = ma_engine_init(&config, engine) - if result != MA_SUCCESS { - throw AudioError.engineInitializationFailed + static func getFromWorld(_ world: borrowing AdaECS.World) -> MiniAudioEngine? { + world.getResource(Self.self) + } + + @unsafe + private final class Engine { + var enginePtr: UnsafeMutablePointer = unsafe .allocate( + capacity: MemoryLayout.size(ofValue: ma_engine.self) + ) + + init() throws { + var config = ma_engine_config_init() + unsafe config.channels = 2 + let result = unsafe ma_engine_init(&config, enginePtr) + if result != MA_SUCCESS { + throw AudioError.engineInitializationFailed + } + } + + deinit { + unsafe ma_engine_uninit(enginePtr) } } - - deinit { - ma_engine_uninit(engine) + + private let engine: Engine + + init() throws { + unsafe self.engine = try Engine() } // MARK: - AudioEngine func start() throws { - let result = ma_engine_start(engine) + let result = unsafe ma_engine_start(engine.enginePtr) if result != MA_SUCCESS { throw MAError.failed("Failed to start", result) } } func stop() throws { - let result = ma_engine_stop(engine) + let result = unsafe ma_engine_stop(engine.enginePtr) if result != MA_SUCCESS { throw MAError.failed("Failed to stop", result) } @@ -58,127 +78,129 @@ final class MiniAudioEngine: AudioEngine, @unchecked Sendable { func update(_ deltaTime: AdaUtils.TimeInterval) { } func makeSound(from url: URL) throws -> Sound { - try MiniSound(from: url, engine: engine) + unsafe try MiniSound(from: url, engine: engine.enginePtr) } func makeSound(from data: Data) throws -> Sound { - try MiniSound(from: data, engine: engine) + unsafe try MiniSound(from: data, engine: engine.enginePtr) } func getAudioListener(at index: Int) -> AudioEngineListener { - if index > ma_engine_get_listener_count(engine) - 1 { + if unsafe index > ma_engine_get_listener_count(engine.enginePtr) - 1 { fatalError("[MiniAudioEngine] Listener not found") } - return MiniAudioEngineListener(engine: engine, listenerIndex: UInt32(index)) + return unsafe MiniAudioEngineListener(engine: engine.enginePtr, listenerIndex: UInt32(index)) } } // MARK: - MiniAudioEngineListener - +@unsafe final class MiniAudioEngineListener: AudioEngineListener, @unchecked Sendable { private let engine: UnsafeMutablePointer let listenerIndex: UInt32 - + init(engine: UnsafeMutablePointer, listenerIndex: UInt32) { - self.engine = engine - self.listenerIndex = listenerIndex + unsafe self.engine = engine + unsafe self.listenerIndex = listenerIndex } var position: Vector3 { get { - let position = ma_engine_listener_get_position(engine, self.listenerIndex) + let position = unsafe ma_engine_listener_get_position(engine, self.listenerIndex) return [position.x, position.y, position.z] } set { - ma_engine_listener_set_position(engine, listenerIndex, newValue.x, newValue.y, newValue.z) + unsafe ma_engine_listener_set_position(engine, listenerIndex, newValue.x, newValue.y, newValue.z) } } var direction: Vector3 { get { - let position = ma_engine_listener_get_direction(engine, listenerIndex) + let position = unsafe ma_engine_listener_get_direction(engine, listenerIndex) return [position.x, position.y, position.z] } set { - ma_engine_listener_set_direction(engine, listenerIndex, newValue.x, newValue.y, newValue.z) + unsafe ma_engine_listener_set_direction(engine, listenerIndex, newValue.x, newValue.y, newValue.z) } } var velocity: Vector3 { get { - let position = ma_engine_listener_get_velocity(engine, listenerIndex) + let position = unsafe ma_engine_listener_get_velocity(engine, listenerIndex) return [position.x, position.y, position.z] } set { - ma_engine_listener_set_velocity(engine, listenerIndex, newValue.x, newValue.y, newValue.z) + unsafe ma_engine_listener_set_velocity(engine, listenerIndex, newValue.x, newValue.y, newValue.z) } } var isEnabled: Bool { get { - return ma_engine_listener_is_enabled(engine, listenerIndex) == 1 + return unsafe ma_engine_listener_is_enabled(engine, listenerIndex) == 1 } set { - ma_engine_listener_set_enabled(engine, listenerIndex, newValue ? 1 : 0) + unsafe ma_engine_listener_set_enabled(engine, listenerIndex, newValue ? 1 : 0) } } var worldUp: Vector3 { get { - let position = ma_engine_listener_get_world_up(engine, listenerIndex) + let position = unsafe ma_engine_listener_get_world_up(engine, listenerIndex) return [position.x, position.y, position.z] } set { - ma_engine_listener_set_world_up(engine, listenerIndex, newValue.x, newValue.y, newValue.z) + unsafe ma_engine_listener_set_world_up(engine, listenerIndex, newValue.x, newValue.y, newValue.z) } } func setCone(innerAngle: Angle, outerAngle: Angle, outerGain: Float) { - ma_engine_listener_set_cone(engine, listenerIndex, innerAngle.radians, outerAngle.radians, outerGain) + unsafe ma_engine_listener_set_cone(engine, listenerIndex, innerAngle.radians, outerAngle.radians, outerGain) } var innerAngle: Angle { var radians: Float = 0 - ma_engine_listener_get_cone(engine, listenerIndex, &radians, nil, nil) + unsafe ma_engine_listener_get_cone(engine, listenerIndex, &radians, nil, nil) return .radians(radians) } var outerAngle: Angle { var radians: Float = 0 - ma_engine_listener_get_cone(engine, listenerIndex, nil, &radians, nil) + unsafe ma_engine_listener_get_cone(engine, listenerIndex, nil, &radians, nil) return .radians(radians) } var outerGain: Float { var gain: Float = 0 - ma_engine_listener_get_cone(engine, listenerIndex, nil, nil, &gain) + unsafe ma_engine_listener_get_cone(engine, listenerIndex, nil, nil, &gain) return gain } } // MARK: - Sound - +@unsafe final class MiniSound: Sound { private(set) var state: SoundState = .ready private var completionHandler: (() -> Void)? - private var sound: UnsafeMutablePointer? = .allocate(capacity: MemoryLayout.size(ofValue: ma_sound.self)) + private var sound: UnsafeMutablePointer? = unsafe .allocate(capacity: MemoryLayout.size(ofValue: ma_sound.self)) init(from fileURL: URL, engine: UnsafeMutablePointer!) throws { let flags = MA_SOUND_FLAG_DECODE.rawValue | MA_SOUND_FLAG_NO_SPATIALIZATION.rawValue - let result = fileURL.path.withCString { pFilePath in - ma_sound_init_from_file(engine, pFilePath, flags, nil, nil, sound) + let result = unsafe fileURL.path.withCString { pFilePath in + unsafe ma_sound_init_from_file(engine, pFilePath, flags, nil, nil, sound) } if result != MA_SUCCESS { throw AudioError.soundInitializationFailed @@ -188,8 +210,8 @@ final class MiniSound: Sound { init(from data: Data, engine: UnsafeMutablePointer!) throws { var data = data let flags = MA_SOUND_FLAG_DECODE.rawValue | MA_SOUND_FLAG_NO_SPATIALIZATION.rawValue - let result = data.withUnsafeMutableBytes { ptr in - ma_sound_init_from_data_source(engine, ptr.baseAddress!, flags, nil, sound) + let result = unsafe data.withUnsafeMutableBytes { ptr in + unsafe ma_sound_init_from_data_source(engine, ptr.baseAddress!, flags, nil, sound) } if result != MA_SUCCESS { @@ -198,8 +220,8 @@ final class MiniSound: Sound { } private init(prototype: MiniSound) throws { - let engine = ma_sound_get_engine(prototype.sound) - let result = ma_sound_init_copy(engine, prototype.sound, 0, nil, sound) + let engine = unsafe ma_sound_get_engine(prototype.sound) + let result = unsafe ma_sound_init_copy(engine, prototype.sound, 0, nil, sound) if result != MA_SUCCESS { throw AudioError.soundInitializationFailed @@ -207,11 +229,11 @@ final class MiniSound: Sound { } deinit { - ma_sound_uninit(sound) + unsafe ma_sound_uninit(sound) } func copy() throws -> Sound { - return try MiniSound(prototype: self) + return unsafe try MiniSound(prototype: self) } func update(_ deltaTime: AdaUtils.TimeInterval) { @@ -220,82 +242,82 @@ final class MiniSound: Sound { var volume: Float { get { - ma_sound_get_volume(sound) + unsafe ma_sound_get_volume(sound) } set { - ma_sound_set_volume(sound, newValue) + unsafe ma_sound_set_volume(sound, newValue) } } var pitch: Float { get { - ma_sound_get_pitch(sound) + unsafe ma_sound_get_pitch(sound) } set { - ma_sound_set_pitch(sound, newValue) + unsafe ma_sound_set_pitch(sound, newValue) } } var position: Vector3 { get { - let position = ma_sound_get_position(sound) + let position = unsafe ma_sound_get_position(sound) return [position.x, position.y, position.z] } set { - ma_sound_set_position(sound, newValue.x, newValue.y, newValue.z) + unsafe ma_sound_set_position(sound, newValue.x, newValue.y, newValue.z) } } var isLooping: Bool { get { - return ma_sound_is_looping(sound) == 1 + return unsafe ma_sound_is_looping(sound) == 1 } set { - ma_sound_set_looping(sound, newValue ? 1 : 0) + unsafe ma_sound_set_looping(sound, newValue ? 1 : 0) } } func start() { - self.state = .playing - ma_sound_start(sound) + unsafe self.state = .playing + unsafe ma_sound_start(sound) } func stop() { - self.state = .stopped - self.stop(resetPlaybackPosition: true, notifyCallback: false) + unsafe self.state = .stopped + unsafe self.stop(resetPlaybackPosition: true, notifyCallback: false) } func pause() { - self.state = .paused - self.stop(resetPlaybackPosition: false, notifyCallback: false) + unsafe self.state = .paused + unsafe self.stop(resetPlaybackPosition: false, notifyCallback: false) } func onCompleteHandler(_ block: @escaping () -> Void) { - let pointer = Unmanaged.passUnretained(self).toOpaque() + let pointer = unsafe Unmanaged.passUnretained(self).toOpaque() - ma_sound_set_end_callback(sound, { userData, _ in - let soundObj = Unmanaged.fromOpaque(userData!).takeUnretainedValue() - soundObj.state = .finished - soundObj.completionHandler?() + unsafe ma_sound_set_end_callback(sound, { userData, _ in + let soundObj = unsafe Unmanaged.fromOpaque(userData!).takeUnretainedValue() + unsafe soundObj.state = .finished + unsafe soundObj.completionHandler?() }, pointer) - self.completionHandler = block + unsafe self.completionHandler = block } // MARK: - Private private func stop(resetPlaybackPosition: Bool, notifyCallback: Bool) { - ma_sound_stop(sound) + unsafe ma_sound_stop(sound) if resetPlaybackPosition { - ma_sound_seek_to_pcm_frame(sound, 0) + unsafe ma_sound_seek_to_pcm_frame(sound, 0) } if notifyCallback { - self.completionHandler?() + unsafe self.completionHandler?() } } } diff --git a/Sources/AdaECS/Commands/Commands.swift b/Sources/AdaECS/Commands/Commands.swift new file mode 100644 index 000000000..910ab6a2a --- /dev/null +++ b/Sources/AdaECS/Commands/Commands.swift @@ -0,0 +1,225 @@ +// +// Commands.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 16.06.2025. +// + +import AdaUtils +import Collections +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public final class WorldCommandQueue: @unchecked Sendable { + var commands: Deque = [] + let lock = NSRecursiveLock() + + public var isEmpty: Bool { + commands.isEmpty + } + + public init() {} + + init(_ commands: Deque) { + self.commands = commands + } + + public func push(_ command: @escaping @Sendable (World) -> Void) { + lock.lock() + defer { lock.unlock() } + commands.append(WorldCommand(applyToWorld: command)) + } + + public func apply(to world: World) { + world.flushCommands() + applyAndDrop(to: world) + } + + public func copy() -> WorldCommandQueue { + WorldCommandQueue(commands) + } + + func applyAndDrop(to world: World) { + while let drop = commands.popFirst() { + drop.applyToWorld(world) + } + } +} + +public struct WorldCommand: Sendable { + let applyToWorld: @Sendable (World) -> Void + + public init(applyToWorld: @escaping @Sendable (World) -> Void) { + self.applyToWorld = applyToWorld + } +} + +@propertyWrapper +public final class Commands: @unchecked Sendable { + public var entities: Entities + public private(set) var queue: WorldCommandQueue + + public var isEmpty: Bool { + self.queue.isEmpty + } + + public var wrappedValue: Commands { + self + } + + public init() { + entities = .init() + queue = .init() + } + + public init(entities: Entities, commandsQueue: WorldCommandQueue) { + self.entities = entities + self.queue = commandsQueue + } +} + +extension Commands: SystemParameter { + public convenience init(from world: World) { + self.init(entities: world.entities, commandsQueue: world.commandQueue) + } + + public func update(from world: World) { + self.entities = world.entities + } + + public func finish(_ world: World) { + queue.apply(to: world) + } +} + +public extension Commands { + func append(_ commands: Commands) { + self.queue.commands.append(contentsOf: commands.queue.commands) + } + + @discardableResult + func spawn( + _ name: String = "", + @ComponentsBuilder components: @escaping @Sendable () -> ComponentsBundle + ) -> EntityCommands { + let entity = entities.allocate(with: name) + self.queue.push { world in + world.insertNewEntity(entity, components: components().components) + } + return EntityCommands(queue: queue, entityId: entity.id) + } + + @discardableResult + func spawn( + _ name: String = "", + bundle: consuming T + ) -> EntityCommands { + let entity = entities.allocate(with: name) + self.queue.push { [bundle] world in + world.insertNewEntity(entity, components: bundle.components) + } + return EntityCommands(queue: queue, entityId: entity.id) + } + + @discardableResult + func spawn(_ name: String = "") -> EntityCommands { + let entity = entities.allocate(with: name) + self.queue.push { world in + world.insertNewEntity(entity, components: []) + } + return EntityCommands(queue: queue, entityId: entity.id) + } + + @discardableResult + func insertEntity(_ entity: Entity) -> EntityCommands { + entities.addNotAllocatedEntity(entity) + queue.push { world in + world.addEntity(entity) + } + return EntityCommands(queue: queue, entityId: entity.id) + } + + @discardableResult + func entity(_ entity: Entity.ID) -> EntityCommands { + EntityCommands(queue: queue, entityId: entity) + } + + func insertResource(_ resource: T) { + self.queue.push { + $0.insertResource(resource) + } + } + + func removeResource(_ resource: T.Type) { + self.queue.push { + $0.removeResource(T.self) + } + } +} + +@safe +public final class EntityCommands { + var queue: WorldCommandQueue + public let entityId: Entity.ID + + init(queue: WorldCommandQueue, entityId: Entity.ID) { + self.queue = queue + self.entityId = entityId + } +} + +public extension EntityCommands { + @discardableResult + func insert(_ component: consuming T) -> Self { + self.queue.push { [component, entityId] world in + world.insert(component, for: entityId) + } + return self + } + + @discardableResult + func remove(_ componentId: ComponentId, from entity: Entity.ID) -> Self { + self.queue.push { world in + world.remove(componentId, from: entity) + } + return self + } + + @discardableResult + @inline(__always) + func addChild( + _ child: Entity + ) -> Self { + self.queue.push { [entityId] world in + let entity = world.getEntityByID(entityId) + entity?.addChild(child) + world.addEntity(child) + } + return self + } + + @inline(__always) + func removeFromWorld(recursively: Bool = false) { + self.queue.push { [entityId] world in + world.removeEntity(entityId, recursively: recursively) + } + } + + @discardableResult + @inline(__always) + func remove(_ component: consuming T) -> Self { + self.remove(T.identifier, from: entityId) + } + + /// Remove a component of the specified type from an entity. + /// - Parameter componentType: The type of component to remove. + /// - Parameter entity: The entity ID to remove the component from. + @discardableResult + @inline(__always) + func remove(_ componentType: T.Type, from entity: Entity.ID) -> Self { + self.remove(T.identifier, from: entity) + } +} diff --git a/Sources/AdaECS/Component/Bundle.swift b/Sources/AdaECS/Component/Bundle.swift new file mode 100644 index 000000000..bec0e5445 --- /dev/null +++ b/Sources/AdaECS/Component/Bundle.swift @@ -0,0 +1,23 @@ +// +// Bundle.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 13.06.2025. +// + +/// Collection of components. +/// +/// Example: +/// ```swift +/// @Bundle +/// struct PlayerBundle { +/// var player: Player +/// } +/// +/// let playerBundle = PlayerBundle(player: Player(name: "John")) +/// world.spawn(bundle: playerBundle) +/// ``` +public protocol ComponentsBundle: Sendable, ~Copyable { + /// The components that are part of the bundle. + var components: [any Component] { get } +} diff --git a/Sources/AdaECS/Component/Component+Runtime.swift b/Sources/AdaECS/Component/Component+Runtime.swift index d8d991c58..1c1bb9938 100644 --- a/Sources/AdaECS/Component/Component+Runtime.swift +++ b/Sources/AdaECS/Component/Component+Runtime.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 5/24/22. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif // TODO: (Vlad) Add components list to editor and generate file with registered components. // TODO: (Vlad) We can think about `swift_getMangledTypeName` and `swift_getTypeByMangledNameInContext` @@ -44,11 +48,11 @@ enum ComponentStorage { /// Return registered component or try to find it by NSClassFromString (works only for objc runtime) static func getRegisteredComponent(for name: String) -> Component.Type? { - return self.registeredComponents[name] ?? (NSClassFromString(name) as? Component.Type) + return unsafe self.registeredComponents[name] ?? (NSClassFromString(name) as? Component.Type) } static func addComponent(_ type: T.Type) { - self.registeredComponents[T.swiftName] = type + unsafe self.registeredComponents[T.swiftName] = type } } diff --git a/Sources/AdaECS/Component/Component.swift b/Sources/AdaECS/Component/Component.swift index 83ffcd811..4dc2cdff8 100644 --- a/Sources/AdaECS/Component/Component.swift +++ b/Sources/AdaECS/Component/Component.swift @@ -12,7 +12,26 @@ import AdaUtils /// The base component in ECS paradigm. /// Component contains data described some entity characteristic in the game world, like: /// color, transformation and etc. -public protocol Component: QueryTarget, ~Copyable { } +public protocol Component: QueryTarget, ~Copyable { + static var componentsInfo: ComponentsInfo { get } +} + +public extension Component { + static var componentsInfo: ComponentsInfo { + ComponentsInfo( + componentId: Self.identifier, + isPlainOldData: _isPOD(Self.self) + ) + } +} + +public struct ComponentsInfo { + public let componentId: ComponentId + + /// Plain struct without any references, ARC, etc. + /// - SeeAlso: _isPOD + public let isPlainOldData: Bool +} /// Provides the events related to components. public enum ComponentEvents { diff --git a/Sources/AdaECS/Component/ComponentsBuilder.swift b/Sources/AdaECS/Component/ComponentsBuilder.swift index 1108c9b6b..480cb0d25 100644 --- a/Sources/AdaECS/Component/ComponentsBuilder.swift +++ b/Sources/AdaECS/Component/ComponentsBuilder.swift @@ -27,7 +27,7 @@ private struct ComponentBuilderTuple: Component { ComponentBuilderTuple(components: component == nil ? [] : [component!]) } - public static func buildArray(_ components: [Component]) -> Component { + public static func buildArray(_ components: [any Component]) -> Component { ComponentBuilderTuple(components: components) } @@ -45,8 +45,8 @@ private struct ComponentBuilderTuple: Component { } // unwrap all components - public static func buildFinalResult(_ component: Component) -> [Component] { - return self.unpackComponentBuilderTuple(component) + public static func buildFinalResult(_ component: Component) -> ComponentsBundle { + return ResultBundle(components: self.unpackComponentBuilderTuple(component)) } private static func unpackComponentBuilderTuple(_ component: Component) -> [Component] { @@ -54,7 +54,7 @@ private struct ComponentBuilderTuple: Component { return [component] } - var components: [Component] = [] + var components: [any Component] = [] for item in tuple.components { if item is ComponentBuilderTuple { @@ -67,3 +67,9 @@ private struct ComponentBuilderTuple: Component { return components } } + +extension ComponentsBuilder { + struct ResultBundle: ComponentsBundle { + var components: [any Component] + } +} diff --git a/Sources/AdaECS/Component/RelationshipComponent.swift b/Sources/AdaECS/Component/RelationshipComponent.swift index d9fbac9d2..b349672c5 100644 --- a/Sources/AdaECS/Component/RelationshipComponent.swift +++ b/Sources/AdaECS/Component/RelationshipComponent.swift @@ -32,10 +32,14 @@ public extension Entity { /// Contains children if has one. var children: [Entity] { - guard let relationship = self.components[RelationshipComponent.self] else { + guard self.components.has(RelationshipComponent.self) else { return [] } - + let relationship = self.components.getOrCreate( + for: RelationshipComponent.self, + default: RelationshipComponent() + ) + return relationship.children.compactMap { self.world?.getEntityByID($0) } @@ -43,10 +47,18 @@ public extension Entity { /// Contains reference for parent entity if available. var parent: Entity? { - guard let relationship = self.components[RelationshipComponent.self], let parent = relationship.parent else { + guard self.components.has(RelationshipComponent.self) else { return nil } + let relationship = self.components.getOrCreate( + for: RelationshipComponent.self, + default: RelationshipComponent() + ) + guard let parent = relationship.parent else { + return nil + } + return self.world?.getEntityByID(parent) } diff --git a/Sources/AdaECS/Component/Resource.swift b/Sources/AdaECS/Component/Resource.swift index 37bb16b62..8b20d4fba 100644 --- a/Sources/AdaECS/Component/Resource.swift +++ b/Sources/AdaECS/Component/Resource.swift @@ -5,11 +5,34 @@ // Created by Vladislav Prusakov on 23.05.2025. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// The singleton resource that passed to the ecs world. /// Only one instance of the resource is allowed in the world. -public protocol Resource: Sendable { } +public protocol Resource: Sendable { + static func getFromWorld(_ world: borrowing World) -> Self? +} + +public extension Resource { + /// Get a resource from the world. + /// - Parameter world: The world to get the resource from. + /// - Returns: The resource if it exists, otherwise nil. + static func getFromWorld(_ world: borrowing World) -> Self? { + world.getResource(Self.self) + } +} + +/// Init the object from a world +public protocol WorldInitable: Sendable { + /// Initialize the resource from a world. + /// - Parameter world: The world to initialize the resource from. + /// - Returns: The initialized resource. + init(from world: World) +} // TODO: (Vlad) Add components list to editor and generate file with registered components. // TODO: (Vlad) We can think about `swift_getMangledTypeName` and `swift_getTypeByMangledNameInContext` @@ -48,10 +71,10 @@ enum ResourceStorage { /// Return registered resource or try to find it by NSClassFromString (works only for objc runtime) static func getRegisteredResource(for name: String) -> Resource.Type? { - return self.registeredResources[name] ?? (NSClassFromString(name) as? Resource.Type) + return unsafe self.registeredResources[name] ?? (NSClassFromString(name) as? Resource.Type) } static func addResource(_ type: T.Type) { - self.registeredResources[T.swiftName] = type + unsafe self.registeredResources[T.swiftName] = type } } diff --git a/Sources/AdaECS/ECSMacros.swift b/Sources/AdaECS/ECSMacros.swift index e4d72ade8..9b97cb44e 100644 --- a/Sources/AdaECS/ECSMacros.swift +++ b/Sources/AdaECS/ECSMacros.swift @@ -28,23 +28,47 @@ @attached(extension, names: arbitrary, conformances: Component) public macro Component() = #externalMacro(module: "AdaEngineMacros", type: "ComponentMacro") + +/// A macro for creating a bundle. +/// A bundle macro is more preffered way to create a bundle. +/// When you use a bundle macro, you will atomatically conforms ``Bundle`` protocol. +/// +/// Example: +/// ```swift +/// @Bundle +/// struct PlayerBundle { +/// var position: Vector3 +/// var player: Player +/// } +/// +/// world.spawn( +/// PlayerBundle( +/// position: [10, 0, 10], +/// player: Player(team: .red) +/// ) +/// ) +/// ``` +@attached(member, names: named(components)) +@attached(extension, names: arbitrary, conformances: ComponentsBundle) +public macro Bundle() = #externalMacro(module: "AdaEngineMacros", type: "BundleMacro") + /// A macro for creating a system. -/// You can pass as many parameters as you want, but they must be a conforms a ``SystemQuery`` protocol. +/// You can pass as many parameters as you want, but they must be a conforms a ``SystemParameter`` protocol. /// /// - Parameters: /// - dependencies: An array of system dependencies. /// /// Example: /// ```swift -/// @System(dependencies: [PhysicsSystem.self]) +/// @PlainSystem(dependencies: [PhysicsSystem.self]) /// struct MovementSystem: System { /// @Query, Velocity> /// private var query /// -/// @ResQuery +/// @Res /// private var resources: Gravity? /// -/// func update(context: inout UpdateContext) { +/// func update(context: UpdateContext) { /// for (transform, velocity) in query { /// transform.position += velocity.value * context.deltaTime /// } @@ -53,20 +77,22 @@ public macro Component() = #externalMacro(module: "AdaEngineMacros", type: "Comp /// ``` @attached(member, names: named(queries), named(dependencies)) @attached(extension, names: arbitrary, conformances: System) -public macro System(dependencies: [SystemDependency] = []) = #externalMacro(module: "AdaEngineMacros", type: "SystemMacro") +public macro PlainSystem( + dependencies: [SystemDependency] = [] +) = #externalMacro(module: "AdaEngineMacros", type: "SystemMacro") /// A macro for creating a system from a function. -/// You can pass as many parameters as you want, but they must be a conforms a ``SystemQuery`` protocol. +/// You can pass as many parameters as you want, but they must be a conforms a ``SystemParameter`` protocol. /// /// - Parameters: /// - dependencies: An array of system dependencies. /// /// Example: /// ```swift -/// @PlainSystem(dependencies: [PhysicsSystem.self]) +/// @System(dependencies: [PhysicsSystem.self]) /// func Movement( /// query: Query, Velocity>, -/// resources: ResQuery, +/// resources: Res, /// ) { /// // ... /// } @@ -76,7 +102,9 @@ public macro System(dependencies: [SystemDependency] = []) = #externalMacro(modu /// } @attached(peer, names: suffixed(System), conformances: System) -public macro PlainSystem(dependencies: [SystemDependency] = []) = #externalMacro(module: "AdaEngineMacros", type: "SystemMacro") +public macro System( + dependencies: [SystemDependency] = [] +) = #externalMacro(module: "AdaEngineMacros", type: "SystemMacro") #endif diff --git a/Sources/AdaECS/Entity/Entity+ComponentSet.swift b/Sources/AdaECS/Entity/Entity+ComponentSet.swift index 1e039a31b..3e79ed1ee 100644 --- a/Sources/AdaECS/Entity/Entity+ComponentSet.swift +++ b/Sources/AdaECS/Entity/Entity+ComponentSet.swift @@ -5,49 +5,48 @@ // Created by v.prusakov on 5/6/22. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import AdaUtils import Collections public extension Entity { - /// Hold entity components specific for entity. struct ComponentSet: Codable, Sendable { @_spi(Internal) - public weak var entity: Entity? - - var world: World? { - return self.entity?.world + public var entity: Entity.ID + + // Reference to world. + weak var world: World? { + didSet { + self.notFlushedComponents.removeAll() + } } - - let lock = NSRecursiveLock() - @_spi(Internal) - @LocalIsolated public private(set) var buffer: OrderedDictionary - private(set) var bitset: BitSet - + /// Components that are not flushed to the world. + var notFlushedComponents: SparseSet = [:] + // MARK: - Codable /// Create an empty component set. - init() { - self.bitset = BitSet() - self.buffer = [:] + init(entity: Entity.ID) { + self.entity = entity } /// Create a component set from another component set. /// - Parameter other: The other component set to create a component set from. init(from other: borrowing Self) { - self.buffer = other.buffer - self.bitset = other.bitset + self.entity = other.entity + self.notFlushedComponents = other.notFlushedComponents } /// Create component set from decoder. public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingName.self) - self.buffer = OrderedDictionary( - minimumCapacity: container.allKeys.count - ) - self.bitset = BitSet(reservingCapacity: container.allKeys.count) + self.entity = -1 for key in container.allKeys { guard let type = ComponentStorage.getRegisteredComponent(for: key.stringValue) @@ -57,8 +56,7 @@ public extension Entity { if let decodable = type as? Decodable.Type { let component = try decodable.init(from: container.superDecoder(forKey: key)) - self.buffer[type.identifier] = component as? Component - self.bitset.insert(type.identifier) + self.notFlushedComponents[type.identifier] = component as? Component } } } @@ -67,143 +65,144 @@ public extension Entity { /// - Parameter encoder: The encoder to encode the component set to. public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingName.self) - for component in self.buffer.elements.values { + guard let world else { + throw CodableError.worldIsNil + } + guard let location = world.entities.entities[entity] else { + throw CodableError.entityNotFoundInWorld + } + + let chunk = world.archetypes + .archetypes[location.archetypeId] + .chunks.chunks[location.chunkIndex] + let components = chunk.getComponents(for: entity) + for (_, component) in components { do { - try container.encode(AnyEncodable(component), forKey: CodingName(stringValue: type(of: component).swiftName)) + try container.encode( + AnyEncodable(component), + forKey: CodingName(stringValue: type(of: component).swiftName) + ) } catch { + // TODO: Logging print("Component encoding error: \(error)") } } } - // FIXME: Replace to subscript?? - /// Get any count of component types from set. - @inline(__always) - public func get(_ type: repeat (each T).Type) -> (repeat each T) { - return (repeat self.buffer[(each type).identifier] as! each T) - } - /// Gets or sets the component of the specified type. + @inline(__always) public subscript(componentType: T.Type) -> T? where T : Component { - get { - return buffer[T.identifier] as? T + _read { + yield get(for: T.self) } - set { if let newValue { - self.set(newValue) + self.insert(newValue) } else { self.remove(T.self) } } } - /// Set the component of the specified type. - public mutating func set(_ component: consuming T) where T : Component { - lock.lock() - defer { - lock.unlock() - } - - let identifier = T.identifier - let isChanged = self.buffer[identifier] != nil + // FIXME: Replace to subscript?? - self.buffer[identifier] = component - self.bitset.insert(T.identifier) - guard let ent = self.entity else { - return - } - - if isChanged { - self.world?.entity(ent, didUpdateComponent: T.self, with: identifier) + /// Get any count of component types from set. + @inline(__always) + public func get(_ type: repeat (each T).Type) -> (repeat each T) { + return (repeat get(for: (each T).self)!) + } + + public func get(for type: T.Type) -> T? { + if let world { + world.get(from: entity) } else { - self.world?.entity(ent, didAddComponent: T.self, with: identifier) + notFlushedComponents[T.identifier] as? T } } - /// Set the components of the specified type. - public mutating func set(_ components: [Component]) { - for component in components { - let componentType = type(of: component) - let identifier = componentType.identifier - let isChanged = self.buffer[identifier] != nil - self.buffer[identifier] = component - self.bitset.insert(identifier) - - guard let ent = self.entity else { - continue - } - lock.lock() - defer { - lock.unlock() - } - if isChanged { - self.world?.entity(ent, didUpdateComponent: componentType, with: identifier) + public func getOrCreate( + for type: T.Type, + default: T + ) -> T { + if let world { + if let value = world.get(T.self, from: entity) { + return value } else { - self.world?.entity(ent, didAddComponent: componentType, with: identifier) + world.insert(`default`, for: entity) + return `default` } + } else { + return notFlushedComponents[T.identifier, default: `default`] as! T + } + } + + /// Set the component of the specified type. + @inline(__always) + public mutating func insert(_ component: consuming T) where T : Component { + guard let world else { + self.notFlushedComponents[T.identifier] = component + return + } + world.insert(component, for: entity) + } + + /// Set the components of the specified type. + @inline(__always) + public mutating func insert(_ components: consuming (repeat (each T))) { + for component in repeat (each components) { + self.insert(component) } } /// Set the components of the specified type using ``ComponentsBuilder``. - public mutating func set(@ComponentsBuilder components: () -> [Component]) { - self.set(components()) + public mutating func insert(@ComponentsBuilder components: () -> [Component]) { + let components = components() + for component in components { + self.insert(component) + } } /// Returns `true` if the collections contains a component of the specified type. - public func has(_ componentType: Component.Type) -> Bool { - return self.buffer[componentType.identifier] != nil + public func has(_ componentType: any Component.Type) -> Bool { + return has(componentType.identifier) } /// Returns `true` if the collections contains a component of the specified type. public func has(_ componentId: ComponentId) -> Bool { - return self.buffer[componentId] != nil + guard let world else { + return self.notFlushedComponents.contains(componentId) + } + return world.has(componentId, in: entity) } /// Removes the component of the specified type from the collection. - public mutating func remove(_ componentType: Component.Type) { - let identifier = componentType.identifier - self.buffer[identifier] = nil - - self.bitset.remove(componentType) - - guard let ent = self.entity else { return } - world?.entity(ent, didRemoveComponent: componentType, with: identifier) - } - - /// Remove all components from set. - public mutating func removeAll(keepingCapacity: Bool = false) { - for component in self.buffer.values.elements { - let componentType = type(of: component) - - guard let ent = self.entity else { return } - world?.entity(ent, didRemoveComponent: componentType, with: componentType.identifier) + public mutating func remove(_ componentType: any Component.Type) { + guard let world else { + self.notFlushedComponents.remove(for: componentType.identifier) + return } - - self.bitset = BitSet(reservingCapacity: self.buffer.count) - self.buffer.removeAll(keepingCapacity: keepingCapacity) + world.remove(componentType.identifier, from: entity) } /// The number of components in the set. public var count: Int { - return self.buffer.count + guard + let world, + let location = world.entities.entities[entity] + else { + return self.notFlushedComponents.count + } + return world.archetypes + .archetypes[location.archetypeId] + .chunks.chunks[location.chunkIndex] + .getComponents(for: entity) + .count } /// A Boolean value indicating whether the set is empty. public var isEmpty: Bool { - return self.buffer.isEmpty - } - - /// Check if a component is changed. - /// - Parameter componentType: The type of the component to check. - /// - Returns: True if the component is changed, otherwise false. - public func isComponentChanged(_ componentType: T.Type) -> Bool { - guard let entity = self.entity else { - return false - } - - return world?.isComponentChanged(componentType, for: entity) ?? false + return count == 0 } /// Copy the component set. @@ -224,8 +223,8 @@ public extension Entity.ComponentSet { @inline(__always) subscript(_ a: A.Type, _ b: B.Type) -> (A, B) where A : Component, B: Component { ( - buffer[a.identifier] as! A, - buffer[b.identifier] as! B + get(for: A.self)!, + get(for: B.self)! ) } @@ -235,11 +234,15 @@ public extension Entity.ComponentSet { /// - Parameter c: The type of the third component. /// - Returns: The components of the specified types. @inline(__always) - subscript(_ a: A.Type, _ b: B.Type, _ c: C.Type) -> (A, B, C) where A : Component, B: Component, C: Component { + subscript( + _ a: A.Type, + _ b: B.Type, + _ c: C.Type + ) -> (A, B, C) where A : Component, B: Component, C: Component { ( - buffer[a.identifier] as! A, - buffer[b.identifier] as! B, - buffer[c.identifier] as! C + get(for: A.self)!, + get(for: B.self)!, + get(for: C.self)! ) } @@ -250,12 +253,17 @@ public extension Entity.ComponentSet { /// - Parameter d: The type of the fourth component. /// - Returns: The components of the specified types. @inline(__always) - subscript(_ a: A.Type, _ b: B.Type, _ c: C.Type, _ d: D.Type) -> (A, B, C, D) where A : Component, B: Component, C: Component, D: Component { + subscript( + _ a: A.Type, + _ b: B.Type, + _ c: C.Type, + _ d: D.Type + ) -> (A, B, C, D) where A : Component, B: Component, C: Component, D: Component { ( - buffer[a.identifier] as! A, - buffer[b.identifier] as! B, - buffer[c.identifier] as! C, - buffer[d.identifier] as! D + get(for: A.self)!, + get(for: B.self)!, + get(for: C.self)!, + get(for: D.self)! ) } } @@ -270,8 +278,19 @@ public extension Entity.ComponentSet { extension Entity.ComponentSet: CustomStringConvertible { public var description: String { - let result = self.buffer.reduce("") { partialResult, value in - let name = type(of: value.value) + guard let world else { + return "ComponentSet(entity: \(entity), world: nil)" + } + guard let location = world.entities.entities[entity] else { + return "ComponentSet(entity: \(entity), world: \(world))" + } + + let chunk = world.archetypes + .archetypes[location.archetypeId] + .chunks.chunks[location.chunkIndex] + let components = chunk.getComponents(for: entity) + let result = components.reduce("") { partialResult, value in + let name = type(of: value.1) return partialResult + "\n ⟐ \(name)" } @@ -284,20 +303,23 @@ extension Entity.ComponentSet { /// - Parameter identifier: The identifier of the component. /// - Returns: The component if it exists, otherwise nil. func get(by identifier: ComponentId) -> T? { - return (self.buffer[identifier] as? T) + if let world { + world.get(T.self, from: entity) + } else { + notFlushedComponents[identifier] as? T + } } /// Get a component by its identifier. /// - Parameter componentId: The identifier of the component. /// - Returns: The component if it exists, otherwise nil. - subscript(by componentId: ComponentId) -> T? where T : Component { - get { - return buffer[T.identifier] as? T + subscript(by componentId: ComponentId) -> T? { + _read { + yield get(T.self) } - set { if let newValue { - self.set(newValue) + self.insert(newValue) } else { self.remove(T.self) } @@ -305,4 +327,11 @@ extension Entity.ComponentSet { } } +private extension Entity { + enum CodableError: Error { + case worldIsNil + case entityNotFoundInWorld + } +} + // swiftlint:enable identifier_name diff --git a/Sources/AdaECS/Entity/Entity.swift b/Sources/AdaECS/Entity/Entity.swift index a194abbf0..e17cef4d1 100644 --- a/Sources/AdaECS/Entity/Entity.swift +++ b/Sources/AdaECS/Entity/Entity.swift @@ -15,14 +15,16 @@ import OrderedCollections /// Entity in ECS based architecture is the main object that holds components. open class Entity: Identifiable, @unchecked Sendable { + static let notAllocatedId = -25102018 + /// Contains entity name. public let name: String /// Contains unique identifier of entity. - public private(set) var id: Int + public package(set) var id: Int /// Contains components specific for current entity. - @LocalIsolated public var components: ComponentSet = ComponentSet() + public var components: ComponentSet /// The dispose bag of the entity. var disposeBag: Set = [] @@ -32,33 +34,40 @@ open class Entity: Identifiable, @unchecked Sendable { public var isActive: Bool = true /// Contains reference for world where entity placed. - public internal(set) weak var world: World? - - /// Create a new entity. - /// Also entity contains next components ``Transform``, ``RelationshipComponent`` and ``Visibility``. - /// - Note: If you want to use entity without any components use ``EmptyEntity`` + public internal(set) weak var world: World? { + didSet { + self.components.world = world + } + } + + /// Create a new entity with not allocated id. To track that entity in the world use ``World/addEntity(_:)`` /// - Parameter name: Name of entity. By default is `Entity`. public init(name: String = "Entity") { self.name = name - self.id = RID().id - // swiftlint:disable:next inert_defer - defer { - self.components.entity = self - } + self.id = Self.notAllocatedId + self.components = ComponentSet(entity: self.id) } - /// Create a new entity and setup components on init. - /// - /// Also entity contains next components ``Transform``, ``RelationshipComponent`` and ``Visibility``. - /// - Note: If you want to use entity without any components use ``EmptyEntity`` + /// Create a new entity with not allocated id. To track that entity in the world use ``World/addEntity(_:)`` /// - Parameter name: Name of entity. By default is `Entity`. - /// - Parameter components: Collection of components. - public convenience init( + public init( name: String = "Entity", - @ComponentsBuilder components: () -> [Component] + @ComponentsBuilder components: () -> ComponentsBundle ) { - self.init(name: name) - self.components.set(components()) + self.name = name + self.id = Self.notAllocatedId + self.components = ComponentSet(entity: self.id) + for component in components().components { + self.components.notFlushedComponents[type(of: component).identifier] = component + } + } + + /// Create a new entity with id. + /// - Parameter name: Name of entity. By default is `Entity`. + init(name: String = "Entity", id: Int) { + self.name = name + self.id = id + self.components = ComponentSet(entity: self.id) } // MARK: - Codable @@ -68,10 +77,9 @@ open class Entity: Identifiable, @unchecked Sendable { let container = try decoder.container(keyedBy: CodingKeys.self) let name = try container.decode(String.self, forKey: .name) let id = try container.decode(Int.self, forKey: .id) - self.init(name: name) - self.id = id + self.init(name: name, id: id) self.components = try container.decode(ComponentSet.self, forKey: .components) - self.components.entity = self + self.components.entity = id } /// Encode the entity to an encoder. @@ -85,18 +93,18 @@ open class Entity: Identifiable, @unchecked Sendable { // MARK: - Public - /// Remove entity from scene. + /// Remove entity from world. /// - Note: Entity will removed on next update tick. - public func removeFromScene(recursively: Bool = false) { + public func removeFromWorld(recursively: Bool = false) { self.world?.removeEntityOnNextTick(self, recursively: recursively) } /// Copy the entity. /// - Returns: A new entity with the same components. open func copy() -> Entity { - let entity = Entity(name: self.name) + let entity = Entity(name: self.name, id: Self.notAllocatedId) entity.components = self.components.copy() - entity.components.entity = entity + entity.components.entity = entity.id entity.isActive = self.isActive return entity } @@ -128,6 +136,19 @@ extension Entity: Codable { } } +extension Entity: CustomStringConvertible { + /// A textual representation of the entity. + public var description: String { + """ + Entity( + id: \(self.id), + name: \(self.name), + components: \(self.components) + ) + """ + } +} + public extension Cancellable { /// Stores this type-erasing cancellable instance in the entity. /// - Note: This cancellable will be canceled when the entity is removed. @@ -139,4 +160,4 @@ public extension Cancellable { entity.disposeBag.insert(AnyCancellable(self)) } } -} \ No newline at end of file +} diff --git a/Sources/AdaECS/Query/ChangeDetectionable.swift b/Sources/AdaECS/Query/ChangeDetectionable.swift new file mode 100644 index 000000000..ae0668638 --- /dev/null +++ b/Sources/AdaECS/Query/ChangeDetectionable.swift @@ -0,0 +1,26 @@ +// +// ChangeDetectionable.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 05.12.2025. +// + +import AdaUtils + +public protocol ChangeDetectionable { + var changeTick: ChangeDetectionTick { get set } + + var isChanged: Bool { get } + + func setChanged() +} + +public extension ChangeDetectionable { + var isChanged: Bool { + return self.changeTick.change?.wrappedValue == self.changeTick.currentTick + } + + func setChanged() { + self.changeTick.change?.getPointer().pointee = self.changeTick.currentTick + } +} diff --git a/Sources/AdaECS/Query/EntityQuery.swift b/Sources/AdaECS/Query/EntityQuery.swift index d21d8f103..2c94faffa 100644 --- a/Sources/AdaECS/Query/EntityQuery.swift +++ b/Sources/AdaECS/Query/EntityQuery.swift @@ -5,14 +5,16 @@ // Created by v.prusakov on 5/24/22. // +import AdaUtils + /// This object describe query to ECS world. /// /// ```swift -/// @System +/// @PlainSystem /// struct MovementSystem { /// @EntityQuery(where: .has(Transform.self)) private var query /// -/// func update(context: inout UpdateContext) { +/// func update(context: UpdateContext) { /// self.query.forEach { /// var transform = entity.components[Transform.self] /// // Do some movement @@ -24,12 +26,12 @@ /// Also, you can combine types in query using `&&` and `||` operators. /// /// ```swift -/// @System +/// @PlainSystem /// struct RendererSystem { /// @EntityQuery(where: .has(SpriteComponent.self) && .has(Transform.self)) /// private var query /// -/// func update(context: inout UpdateContext) { +/// func update(context: UpdateContext) { /// for entity in self.query { /// // Get components from entity and do some render /// } @@ -38,7 +40,8 @@ /// ``` @propertyWrapper @frozen public struct EntityQuery: Sendable { - public typealias Result = QueryResult> + + public typealias Result = QueryResult, NoFilter> public var wrappedValue: Result { return QueryResult(state: self.state) @@ -58,7 +61,7 @@ } public init(from world: World) { - fatalError() + fatalError("Can't initialize EntityQuery from world") } public func callAsFunction() -> Result { @@ -66,26 +69,28 @@ } } -extension EntityQuery: SystemQuery { +extension EntityQuery: SystemParameter { + public func finish(_ world: World) { } + public func update(from world: World) { self.state.updateArchetypes(in: world) } } +// TODO: Make deprecated + /// This iterator iterate by each entity in passed archetype array public struct EntityIterator: IteratorProtocol { - // We use pointer to avoid additional allocation in memory let count: Int let state: QueryState private var currentArchetypeIndex = 0 private var currentEntityIndex = -1 // We should use -1 for first iterating. - private var canIterateNext: Bool = true /// - Parameter pointer: Pointer to archetypes array. /// - Parameter count: Count archetypes in array. init(state: QueryState) { - self.count = state.archetypes.count + self.count = state.archetypeIndecies.count self.state = state } @@ -94,14 +99,25 @@ public struct EntityIterator: IteratorProtocol { guard self.count > 0 else { return nil } - + + guard let world = state.world else { + return nil + } + while true { guard self.currentArchetypeIndex < self.count else { return nil } - let currentEntitiesCount = self.state.archetypes[self.currentArchetypeIndex].entities.count + let currentArchetypeIndex = self.state.archetypeIndecies[self.currentArchetypeIndex] + + // Validate archetype index is within bounds + guard currentArchetypeIndex < world.archetypes.archetypes.count else { + self.currentArchetypeIndex += 1 + continue + } + let currentEntitiesCount = world.archetypes.archetypes[currentArchetypeIndex].entities.count if self.currentEntityIndex < currentEntitiesCount - 1 { self.currentEntityIndex += 1 } else { @@ -110,9 +126,9 @@ public struct EntityIterator: IteratorProtocol { continue } - let currentArchetype = self.state.archetypes[self.currentArchetypeIndex] - - guard let entity = currentArchetype.entities[self.currentEntityIndex], entity.isActive else { + let currentArchetype = self.state.archetypeIndecies[self.currentArchetypeIndex] + let entity = world.archetypes.archetypes[currentArchetype].entities[self.currentEntityIndex] + guard entity.isActive else { continue } diff --git a/Sources/AdaECS/Query/LocalIsolated+SystemQuery.swift b/Sources/AdaECS/Query/LocalIsolated+SystemQuery.swift index e927f4f96..534670e6b 100644 --- a/Sources/AdaECS/Query/LocalIsolated+SystemQuery.swift +++ b/Sources/AdaECS/Query/LocalIsolated+SystemQuery.swift @@ -7,14 +7,104 @@ import AdaUtils -extension LocalIsolated: SystemQuery { +@propertyWrapper +@dynamicMemberLookup +public final class Local { - convenience public init(from world: World) { + public var wrappedValue: Value { + _read { + yield _value + } + _modify { + yield &_value + } + } + + private nonisolated var _value: Value + + /// Initializes lock-isolated state around a value. + /// + /// - Parameter value: A value to isolate with a lock. + public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { + self._value = try value() + } + + /// Get a dynamic member from the isolated value. + /// - Parameter keyPath: The key path to the dynamic member. + /// - Returns: The dynamic member. + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> Subject { + _read { + yield _value[keyPath: keyPath] + } + _modify { + yield &_value[keyPath: keyPath] + } + } + + /// Initialize a new isolated value. + /// - Parameter value: The value to isolate. + public init(wrappedValue value: consuming Value) { + self._value = value + } +} + +extension Local: Sendable where Value: Sendable {} +extension Local: Hashable where Value: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(_value) + } +} +extension Local: Equatable where Value: Equatable { + public static func == (lhs: Local, rhs: Local) -> Bool { + lhs._value == rhs._value + } +} + +extension Local: SystemParameter { + public convenience init(from world: World) { fatalError("Can't be initialized from world") } /// Updates the query state with the given world. - public func update(from world: World) { + public func update(from world: World) { } + + public func finish(_ world: World) { } +} + +extension Local: ExpressibleByBooleanLiteral where Value == Bool { + public convenience init(booleanLiteral value: BooleanLiteralType) { + self.init(value) + } +} + +extension Local: ExpressibleByUnicodeScalarLiteral where Value == String { + public convenience init(unicodeScalarLiteral value: String) { + self.init(value) + } +} + +extension Local: ExpressibleByExtendedGraphemeClusterLiteral where Value == String { + public convenience init(extendedGraphemeClusterLiteral value: String) { + self.init(value) + } +} + +extension Local: ExpressibleByStringLiteral where Value == String { + public convenience init(stringLiteral value: StringLiteralType) { + self.init(value) + } +} + +extension Local: ExpressibleByIntegerLiteral where Value == Int { + public convenience init(integerLiteral value: IntegerLiteralType) { + self.init(value) + } +} +extension Local: ExpressibleByFloatLiteral where Value == Float { + public convenience init(floatLiteral value: FloatLiteralType) { + self.init(Float(value)) } } diff --git a/Sources/AdaECS/Query/ParallelQueryResult.swift b/Sources/AdaECS/Query/ParallelQueryResult.swift new file mode 100644 index 000000000..750f16f4e --- /dev/null +++ b/Sources/AdaECS/Query/ParallelQueryResult.swift @@ -0,0 +1,263 @@ +// +// ParallelQueryResult.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 21.11.2025. +// + +import AdaUtils + +/// Information about a chunk location for parallel processing. +private struct ChunkInfo: Sendable { + let archetypeIndex: Int + let chunkIndex: Int +} + +/// A parallel query processor that iterates over chunks concurrently. +/// +/// Use this to process query results in parallel across multiple threads. +/// The batch size determines how many chunks are processed in a single task. +/// +/// ```swift +/// // Process entities in parallel +/// await query.parallel(batchSize: 4).forEach { position, velocity in +/// // Process each entity concurrently +/// position.x += velocity.x +/// } +/// ``` +public struct ParallelQueryResult: Sendable { + public typealias Element = B.Components + public typealias Filter = QueryBuilderTargets + + let state: QueryState + let batchSize: Int + + /// Create a new parallel query processor. + /// - Parameters: + /// - state: The query state containing archetype indices and world reference + /// - batchSize: Number of chunks to process per task (default: 4) + init(state: QueryState, batchSize: Int) { + self.state = state + self.batchSize = batchSize + } + + /// Process each element in parallel using a TaskGroup. + /// - Parameter operation: The operation to perform on each element + @concurrent + public func forEach( + _ operation: @escaping @Sendable (Element) async throws -> Void + ) async rethrows where Element: Sendable { + let batches = collectBatches() + let state = self.state + + try await withThrowingTaskGroup(of: Void.self) { group in + for batch in batches { + group.addTask { [state] in + try await Self.processBatch(batch, state: state, operation: operation) + } + } + + for try await _ in group { } + } + } + + /// Map each element in parallel and collect results. + /// - Parameter transform: The transformation to apply to each element + /// - Returns: Array of transformed results + @concurrent + public func map( + _ transform: @escaping @Sendable (Element) async throws -> T + ) async rethrows -> [T] { + let batches = collectBatches() + let state = self.state + + return try await withThrowingTaskGroup(of: [T].self) { group in + for batch in batches { + group.addTask { [state] in + try await Self.mapBatch(batch, state: state, transform: transform) + } + } + + var results: [T] = [] + for try await batchResults in group { + results.append(contentsOf: batchResults) + } + return results + } + } + + /// Collect all chunks into batches for parallel processing. + @inline(__always) + private func collectBatches() -> [[ChunkInfo]] { + guard let world = state.world else { + return [] + } + + var allChunks: [ChunkInfo] = [] + + // Collect all chunks from all matching archetypes + for archetypeIndex in state.archetypeIndecies { + guard archetypeIndex < world.archetypes.archetypes.count else { + continue + } + + let archetype = world.archetypes.archetypes[archetypeIndex] + for chunkIndex in 0.. Void + ) async rethrows { + guard let world = state.world else { + return + } + + let states = B.initState(world: world) + var fetches = B.initFetches(world: world, states: states, lastTick: world.lastTick) + let filterStates = Filter.initState(world: world) + var filterFetchs = Filter.initFetches(world: world, states: filterStates, lastTick: world.lastTick) + + for chunkInfo in batch { + let archetypes = world.archetypes + guard chunkInfo.archetypeIndex < archetypes.archetypes.count else { + continue + } + + let archetype = archetypes.archetypes[chunkInfo.archetypeIndex] + guard chunkInfo.chunkIndex < archetype.chunks.chunks.count else { + continue + } + + let chunk = archetype.chunks.chunks[chunkInfo.chunkIndex] + B.setChunk( + states: states, + fetches: &fetches, + chunk: chunk, + archetype: archetype + ) + Filter.setChunk( + states: filterStates, + fetches: &filterFetchs, + chunk: chunk, + archetype: archetype + ) + + // Iterate over all entities in this chunk + for row in 0..( + _ batch: [ChunkInfo], + state: QueryState, + transform: @Sendable (Element) async throws -> T + ) async rethrows -> [T] { + guard let world = state.world else { + return [] + } + + let states = B.initState(world: world) + var fetches = B.initFetches(world: world, states: states, lastTick: world.lastTick) + let filterStates = Filter.initState(world: world) + var filterFetchs = Filter.initFetches(world: world, states: filterStates, lastTick: world.lastTick) + + var results: [T] = [] + + for chunkInfo in batch { + let archetypes = world.archetypes + guard chunkInfo.archetypeIndex < archetypes.archetypes.count else { + continue + } + + let archetype = archetypes.archetypes[chunkInfo.archetypeIndex] + guard chunkInfo.chunkIndex < archetype.chunks.chunks.count else { + continue + } + + let chunk = archetype.chunks.chunks[chunkInfo.chunkIndex] + B.setChunk( + states: states, + fetches: &fetches, + chunk: chunk, + archetype: archetype + ) + Filter.setChunk( + states: filterStates, + fetches: &filterFetchs, + chunk: chunk, + archetype: archetype + ) + + // Iterate over all entities in this chunk + for row in 0.. = FilterQuery { +public struct FilterQuery: Sequence, Sendable { + /// The element type of the query result. + public typealias Element = Builder.Components + + /// The iterator type of the query result. + public typealias Iterator = FilterQueryIterator> - public typealias Builder = QueryBuilderTargets + public typealias Builder = QueryBuilderTargets - public var wrappedValue: QueryResult { - .init(state: self.state) + public var wrappedValue: Self { + _read { yield self } } let state: QueryState - let filter: QueryFilter /// Create a new query for specific predicate. /// - Parameter predicate: Describe what entity should contains to satisfy query. - /// - Parameter filter: Describe filter of this query. By default is ``Filter/all`` - public init(filter: QueryFilter = .all) { - self.filter = filter + public init() { self.state = QueryState( predicate: .init( evaluate: { Builder.predicate(in: $0) } ), - filter: filter + filter: .all ) } public init(from world: World) { - self.init(filter: .all) + self.state = QueryState( + predicate: .init( + evaluate: { Builder.predicate(in: $0) } + ), + filter: .all + ) + self.state.updateArchetypes(in: world) + } +} + +/// Contains array of entities matched for the given EntityQuery request. +extension FilterQuery { + + /// Returns first element of collection. + public var first: Element? { + return self.first { _ in return true } + } + + /// Calculate count of element in collection + /// - Complexity: O(n) + public var count: Int { + return self.count { _ in return true } + } + + /// A Boolean value indicating whether the collection is empty. + public var isEmpty: Bool { + return self.state.archetypeIndecies.isEmpty + } + + public func makeIterator() -> Iterator { + Iterator(state: self.state) } - public func callAsFunction() -> QueryResult { - .init(state: self.state) + /// Returns a parallel query processor for concurrent iteration over chunks. + /// + /// Use this method to process query results in parallel across multiple threads. + /// The batch size determines how many chunks are grouped together in a single task, + /// which helps balance the workload across available CPU cores. + /// + /// ```swift + /// // Process entities in parallel with custom batch size + /// await query.parallel(batchSize: 8).forEach { position, velocity in + /// position.x += velocity.x * deltaTime + /// position.y += velocity.y * deltaTime + /// } + /// + /// // Map entities in parallel and collect results + /// let distances = await query.parallel().map { position in + /// return sqrt(position.x * position.x + position.y * position.y) + /// } + /// ``` + /// + /// - Parameter batchSize: Number of chunks to process per task. Default is 4. + /// Larger values reduce task overhead but may cause load imbalance. + /// Smaller values provide better load distribution but increase task overhead. + /// - Returns: A ``ParallelQueryResult`` instance for concurrent processing + public func parallel(batchSize: Int = 4) -> ParallelQueryResult { + return ParallelQueryResult(state: self.state, batchSize: batchSize) } } -extension FilterQuery: SystemQuery { +extension FilterQuery: SystemParameter { public func update(from world: World) { self.state.updateArchetypes(in: world) } @@ -84,7 +146,16 @@ extension FilterQuery: SystemQuery { @usableFromInline final class QueryState: @unchecked Sendable { @usableFromInline - private(set) var archetypes: [Archetype] = [] + private(set) var archetypeIndecies: [Int] = [] + + @usableFromInline + private(set) unowned var entities: Entities! + + @usableFromInline + private(set) unowned var world: World! + + @usableFromInline + private(set) var lastTick: Tick = Tick(value: 0) @usableFromInline let predicate: QueryPredicate @@ -92,15 +163,194 @@ final class QueryState: @unchecked Sendable { @usableFromInline let filter: QueryFilter - private(set) weak var world: World? - + @usableFromInline internal init(predicate: QueryPredicate, filter: QueryFilter) { self.predicate = predicate self.filter = filter } + @usableFromInline func updateArchetypes(in world: World) { + self.entities = world.entities + self.archetypeIndecies = world.archetypes.archetypes.enumerated().compactMap { + self.predicate.evaluate($0.element) ? $0.offset : nil + } + self.lastTick = world.lastTick self.world = world - self.archetypes = world.archetypes.filter { self.predicate.evaluate($0) } + } +} + +/// This iterator iterate by each entity in passed archetype array +public struct FilterQueryIterator< + B: QuertyTargetBuilder, + F: FilterTargetBuilder +>: IteratorProtocol { + public typealias Element = B.Components + + @usableFromInline + struct Cursor { + @usableFromInline + var currentArchetypeIndex = 0 + + @usableFromInline + var currentChunkIndex = 0 + + /// Current row in chunk + @usableFromInline + var currentRow = 0 + + /// Current length in chunk + @usableFromInline + var currentLength = 0 + } + + @usableFromInline + unowned let archetypes: Archetypes + + @usableFromInline + let count: Int + + @usableFromInline + let state: QueryState + + @usableFromInline + var cursor: Cursor + + @usableFromInline + var fetches: B.ComponentsFetches + + @usableFromInline + var states: B.ComponentsStates + + @usableFromInline + var filterStates: F.ComponentsStates + + @usableFromInline + var filterFetches: F.ComponentsFetches + + @usableFromInline + var needsUpdateData = true + + @usableFromInline + init(state: QueryState) { + self.count = state.archetypeIndecies.count + self.state = state + self.cursor = Cursor() + self.archetypes = state.world.archetypes + + self.states = B.initState(world: state.world) + self.fetches = B.initFetches( + world: state.world, + states: self.states, + lastTick: state.lastTick + ) + self.filterStates = F.initState(world: state.world) + self.filterFetches = F.initFetches( + world: state.world, + states: filterStates, + lastTick: state.lastTick + ) + } + + @inlinable + public mutating func next() -> Element? { + // swiftlint:disable:next empty_count + guard count > 0 && !archetypes.archetypes.isEmpty else { + return nil + } + + while true { + guard cursor.currentArchetypeIndex < self.count else { + return nil + } + + let archetypeIndex = state.archetypeIndecies[cursor.currentArchetypeIndex] + let archetype = self.archetypes.archetypes[archetypeIndex] + + if archetype.isEmpty { + cursor.currentArchetypeIndex += 1 + cursor.currentChunkIndex = 0 + needsUpdateData = true + continue + } + + if cursor.currentChunkIndex >= archetype.chunks.chunks.count { + cursor.currentArchetypeIndex += 1 + cursor.currentChunkIndex = 0 + cursor.currentRow = 0 + needsUpdateData = true + updateStates() + continue + } + + if cursor.currentRow >= archetype.chunks.chunks[cursor.currentChunkIndex].count { + cursor.currentChunkIndex += 1 + cursor.currentRow = 0 + needsUpdateData = true + updateStates() + continue + } + + let currentChunk = archetype.chunks.chunks[cursor.currentChunkIndex] + if needsUpdateData { + B.setChunk( + states: states, + fetches: &fetches, + chunk: currentChunk, + archetype: archetype + ) + F.setChunk( + states: filterStates, + fetches: &filterFetches, + chunk: currentChunk, + archetype: archetype + ) + needsUpdateData = false + } + + let entityId = currentChunk.entities[cursor.currentRow] + + defer { + cursor.currentRow += 1 + } + + guard F.condition( + states: filterStates, + fetches: filterFetches, + at: cursor.currentRow + ) else { + continue + } + + guard let location = state.entities.entities[entityId] else { + continue + } + let entity = archetype.entities[location.archetypeRow] + + if let value = B.getQueryTargets( + for: entity, + states: states, + fetches: fetches, + at: cursor.currentRow + ) { + return value + } + } + } + + @usableFromInline + mutating func updateStates() { + states = B.initState(world: state.world) + fetches = B.initFetches( + world: state.world, + states: states, + lastTick: state.lastTick + ) + filterStates = F.initState(world: state.world) + filterFetches = F.initFetches( + world: state.world, + states: filterStates, + lastTick: state.lastTick + ) } } diff --git a/Sources/AdaECS/Query/QueryBuilder.swift b/Sources/AdaECS/Query/QueryBuilder.swift index 55724b379..715ae667e 100644 --- a/Sources/AdaECS/Query/QueryBuilder.swift +++ b/Sources/AdaECS/Query/QueryBuilder.swift @@ -6,7 +6,7 @@ // /// A protocol for building queries. -public protocol QueryBuilder { +public protocol QueryBuilder: Sendable { /// The components of the query builder. associatedtype Components @@ -14,22 +14,105 @@ public protocol QueryBuilder { /// The component types of the query builder. associatedtype ComponentTypes + /// The component fetches of the query builder. + associatedtype ComponentsFetches + + /// The component states of the query builder. + associatedtype ComponentsStates + + static func initState(world: World) -> ComponentsStates + + static func setChunk( + states: ComponentsStates, + fetches: inout ComponentsFetches, + chunk: borrowing Chunk, + archetype: borrowing Archetype + ) + + static func initFetches( + world: World, + states: ComponentsStates, + lastTick: Tick + ) -> ComponentsFetches +} + +/// A protocol for building target queries. +public protocol QuertyTargetBuilder: QueryBuilder { + static func getQueryTargets( + for entity: Entity, + states: ComponentsStates, + fetches: ComponentsFetches, + at row: Int + ) -> Components? + /// Predicate for the query builder. /// - Parameter archetype: The archetype to check. /// - Returns: True if the archetype satisfies the predicate, otherwise false. - static func predicate(in archetype: Archetype) -> Bool + static func predicate(in archetype: borrowing Archetype) -> Bool +} + +/// A protocol for building filter queries. +public protocol FilterTargetBuilder: QueryBuilder { + static func condition( + states: ComponentsStates, + fetches: ComponentsFetches, + at row: Int + ) -> Bool +} - /// Get the query target from an entity. - /// - Parameter entity: The entity to get the query target from. - /// - Returns: The query target. - static func getQueryTarget(from entity: Entity) -> Components +@usableFromInline +enum QueryBuilderTargetsError: Swift.Error { + case failedToFetch } /// A type-erased query builder. -public struct QueryBuilderTargets: QueryBuilder where repeat each T: QueryTarget { +public struct QueryBuilderTargets: QueryBuilder where repeat each T: WorldQueryTarget { public typealias ComponentTypes = (repeat (each T).Type) public typealias Components = (repeat each T) + public typealias ComponentsFetches = (repeat (each T).Fetch) + public typealias ComponentsStates = (repeat (each T).State) + + @inlinable + @inline(__always) + public static func initState(world: World) -> ComponentsStates { + (repeat (each T)._initState(world: world)) + } + + @inlinable + @inline(__always) + public static func initFetches( + world: World, + states: ComponentsStates, + lastTick: Tick + ) -> ComponentsFetches { + return (repeat (each T)._initFetch( + world: world, + state: each states, + lastTick: lastTick, + currentTick: world.lastTick + )) + } + + @inlinable + @inline(__always) + public static func setChunk( + states: ComponentsStates, + fetches: inout ComponentsFetches, + chunk: borrowing Chunk, + archetype: borrowing Archetype + ) { + fetches = (repeat (each T)._setData( + state: each states, + fetch: each fetches, + chunk: chunk, + archetype: archetype + )) + } +} +extension QueryBuilderTargets: QuertyTargetBuilder where repeat each T: QueryTarget { + @inlinable + @inline(__always) public static func predicate(in archetype: Archetype) -> Bool { for element in repeat (each T).self { if !element._queryContains(in: archetype) { @@ -37,10 +120,45 @@ public struct QueryBuilderTargets: QueryBuilder where repeat } } - return F.condition(for: archetype) + return true } - public static func getQueryTarget(from entity: Entity) -> Components { - (repeat (each T)._queryTarget(from: entity)) + @inlinable + @inline(__always) + public static func getQueryTargets( + for entity: Entity, + states: ComponentsStates, + fetches: ComponentsFetches, + at row: Int + ) -> Components? { + @inline(__always) + func fetch(_ type: Q.Type, state: Q.State, fetch: Q.Fetch) throws -> Q { + guard let value = Q._queryFetch(for: entity, state: state, fetch: fetch, at: row) else { + throw QueryBuilderTargetsError.failedToFetch + } + return value + } + do { + return try (repeat fetch((each T).self, state: each states, fetch: each fetches)) + } catch { + return nil + } + } +} + +extension QueryBuilderTargets: FilterTargetBuilder where repeat each T: Filter { + @inlinable + @inline(__always) + public static func condition( + states: ComponentsStates, + fetches: ComponentsFetches, + at row: Int + ) -> Bool { + for (filter, state, fetch) in repeat ((each T).self, each states, each fetches) { + if !filter.condition(state: state, fetch: fetch, at: row) { + return false + } + } + return true } } diff --git a/Sources/AdaECS/Query/QueryFilter.swift b/Sources/AdaECS/Query/QueryFilter.swift index 0266d150f..776972a25 100644 --- a/Sources/AdaECS/Query/QueryFilter.swift +++ b/Sources/AdaECS/Query/QueryFilter.swift @@ -33,45 +33,401 @@ public struct QueryFilter: OptionSet, Sendable { } /// A protocol for filters. -public protocol Filter: Sendable { +public protocol Filter: Sendable, WorldQueryTarget { /// Check if the filter is satisfied for an archetype. /// - Parameter archetype: The archetype to check. /// - Returns: True if the filter is satisfied for the archetype, otherwise false. - static func condition(for archetype: Archetype) -> Bool + @inlinable + static func condition( + state: State, + fetch: Fetch, + at row: Int + ) -> Bool } /// A filter that includes entities with a specific component. public struct With: Filter { - public static func condition(for archetype: Archetype) -> Bool { - archetype.componentsBitMask.contains(T.self) + public typealias State = Void + public typealias Fetch = ComponentMaskSet + + @inlinable + public static func _initState(world: World) -> Void { } + + @inlinable + public static func _initFetch(world: World, state: Void, lastTick: Tick, currentTick: Tick) -> ComponentMaskSet { + ComponentMaskSet() + } + + @inlinable + public static func _setData( + state: Void, + fetch: ComponentMaskSet, + chunk: Chunk, + archetype: Archetype + ) -> ComponentMaskSet { + archetype.componentLayout.maskSet + } + + @inlinable + @inline(__always) + public static func condition( + state: State, + fetch: ComponentMaskSet, + at row: Int + ) -> Bool { + return fetch.contains(T.self) } } /// A filter that excludes entities with a specific component. -public struct WithOut: Filter { - public static func condition(for archetype: Archetype) -> Bool { - !archetype.componentsBitMask.contains(T.self) +public struct Without: Filter { + public typealias State = Void + public typealias Fetch = ComponentMaskSet + + @inlinable + public static func _initState(world: World) -> Void { } + + @inlinable + public static func _initFetch(world: World, state: Void, lastTick: Tick, currentTick: Tick) -> ComponentMaskSet { + ComponentMaskSet() + } + + @inlinable + public static func _setData( + state: Void, + fetch: ComponentMaskSet, + chunk: Chunk, + archetype: Archetype + ) -> ComponentMaskSet { + archetype.componentLayout.maskSet + } + + @inlinable + @inline(__always) + public static func condition(state: Void, fetch: ComponentMaskSet, at row: Int) -> Bool { + return !fetch.contains(T.self) } } /// A filter that combines two filters with a logical AND operation. -public struct And: Filter { - public static func condition(for archetype: Archetype) -> Bool { - T.condition(for: archetype) && U.condition(for: archetype) +public struct And: Filter { + public typealias State = _State + public typealias Fetch = _Fetch + + public struct _State: Sendable { + @usableFromInline + var states: (repeat (each T).State) + + @usableFromInline + init(states: (repeat (each T).State)) { + self.states = states + } + } + + public struct _Fetch { + @usableFromInline + var fetches: (repeat (each T).Fetch) + + @usableFromInline + init(fetches: (repeat (each T).Fetch)) { + self.fetches = fetches + } + } + + @inlinable + public static func _initState(world: World) -> _State { + _State( + states: (repeat (each T)._initState(world: world)) + ) + } + + @inlinable + public static func _initFetch(world: World, state: _State, lastTick: Tick, currentTick: Tick) -> _Fetch { + _Fetch( + fetches: (repeat (each T)._initFetch( + world: world, + state: each state.states, + lastTick: lastTick, + currentTick: currentTick) + ) + ) + } + + @inlinable + public static func _setData( + state: _State, + fetch: _Fetch, + chunk: Chunk, + archetype: Archetype + ) -> _Fetch { + var newFetch = fetch + newFetch.fetches = (repeat (each T)._setData( + state: each state.states, + fetch: each fetch.fetches, + chunk: chunk, + archetype: archetype) + ) + return newFetch + } + + @inlinable + @inline(__always) + public static func condition( + state: State, + fetch: Fetch, + at row: Int + ) -> Bool { + for (filter, state, fetch) in repeat ((each T).self, each state.states, each fetch.fetches) { + if !filter.condition(state: state, fetch: fetch, at: row) { + return false + } + } + return true + } +} + +public struct Not: Filter { + public typealias State = T.State + public typealias Fetch = T.Fetch + + @inlinable + public static func _initState(world: World) -> T.State { + T._initState(world: world) + } + + @inlinable + public static func _initFetch(world: World, state: T.State, lastTick: Tick, currentTick: Tick) -> T.Fetch { + T._initFetch(world: world, state: state, lastTick: lastTick, currentTick: currentTick) + } + + @inlinable + public static func _setData(state: T.State, fetch: T.Fetch, chunk: Chunk, archetype: Archetype) -> T.Fetch { + T._setData(state: state, fetch: fetch, chunk: chunk, archetype: archetype) + } + + @inlinable + @inline(__always) + public static func condition(state: T.State, fetch: T.Fetch, at row: Int) -> Bool { + !T.condition(state: state, fetch: fetch, at: row) } } /// A filter that combines two filters with a logical OR operation. -public struct Or: Filter { - public static func condition(for archetype: Archetype) -> Bool { - T.condition(for: archetype) || U.condition(for: archetype) +public struct Or: Filter { + public typealias State = _State + public typealias Fetch = _Fetch + + public struct _State: Sendable { + @usableFromInline + var states: (repeat (each T).State) + + @usableFromInline + init(states: (repeat (each T).State)) { + self.states = states + } + } + + public struct _Fetch { + @usableFromInline + var fetches: (repeat (each T).Fetch) + + @usableFromInline + init(fetches: (repeat (each T).Fetch)) { + self.fetches = fetches + } + } + + @inlinable + public static func _initState(world: World) -> _State { + _State( + states: (repeat (each T)._initState(world: world)) + ) + } + + @inlinable + public static func _initFetch(world: World, state: _State, lastTick: Tick, currentTick: Tick) -> _Fetch { + _Fetch( + fetches: (repeat (each T)._initFetch( + world: world, + state: each state.states, + lastTick: lastTick, + currentTick: currentTick) + ) + ) + } + + @inlinable + public static func _setData(state: _State, fetch: _Fetch, chunk: Chunk, archetype: Archetype) -> _Fetch { + var newFetch = fetch + newFetch.fetches = (repeat (each T)._setData( + state: each state.states, + fetch: each fetch.fetches, + chunk: chunk, + archetype: archetype) + ) + return newFetch + } + + @inlinable + @inline(__always) + public static func condition( + state: State, + fetch: Fetch, + at row: Int + ) -> Bool { + for (filter, state, fetch) in repeat ((each T).self, each state.states, each fetch.fetches) { + if filter.condition(state: state, fetch: fetch, at: row) { + return true + } + } + return false + } +} + +public struct Changed: Filter { + @safe + public struct ChangedFetch { + @usableFromInline + var ticks: UnsafeMutablePointer? + @usableFromInline + var lastTick: Tick + @usableFromInline + var currentTick: Tick + + @usableFromInline + init( + ticks: UnsafeMutablePointer? = nil, + lastTick: Tick, + currentTick: Tick + ) { + unsafe self.ticks = ticks + self.lastTick = lastTick + self.currentTick = currentTick + } + } + + public typealias State = Void + public typealias Fetch = ChangedFetch + + @inlinable + public static func _initState(world: World) -> Void { } + + @inlinable + public static func _initFetch( + world: World, + state: Void, + lastTick: Tick, + currentTick: Tick + ) -> ChangedFetch { + ChangedFetch(lastTick: lastTick, currentTick: currentTick) + } + + @inlinable + public static func _setData( + state: Void, + fetch: ChangedFetch, + chunk: Chunk, + archetype: Archetype + ) -> ChangedFetch { + var newFetch = fetch + guard let slice = chunk.getMutableComponentTicksSlice(for: T.self) else { + return fetch + } + unsafe newFetch.ticks = slice + return newFetch + } + + @inlinable + @inline(__always) + public static func condition(state: Void, fetch: ChangedFetch, at row: Int) -> Bool { + guard let tick = unsafe fetch.ticks?.advanced(by: row).pointee else { + return false + } + return tick >= fetch.lastTick + } +} + +public struct Added: Filter { + @safe + public struct AddedFetch { + @usableFromInline + var ticks: UnsafeMutablePointer? + @usableFromInline + var lastTick: Tick + @usableFromInline + var currentTick: Tick + + @usableFromInline + init( + ticks: UnsafeMutablePointer? = nil, + lastTick: Tick, + currentTick: Tick + ) { + unsafe self.ticks = ticks + self.lastTick = lastTick + self.currentTick = currentTick + } + } + + public typealias State = Void + public typealias Fetch = AddedFetch + + @inlinable + public static func _initState(world: World) -> Void { } + + @inlinable + public static func _initFetch( + world: World, + state: Void, + lastTick: Tick, + currentTick: Tick + ) -> AddedFetch { + AddedFetch(lastTick: lastTick, currentTick: currentTick) + } + + @inlinable + public static func _setData( + state: Void, + fetch: AddedFetch, + chunk: Chunk, + archetype: Archetype + ) -> AddedFetch { + var newFetch = fetch + guard let slice = chunk.getMutableComponentTicksSlice(for: T.self) else { + return fetch + } + unsafe newFetch.ticks = slice + return newFetch + } + + @inlinable + @inline(__always) + public static func condition(state: Void, fetch: AddedFetch, at row: Int) -> Bool { + guard let tick = unsafe fetch.ticks?.advanced(by: row).pointee else { + return false + } + return tick == fetch.lastTick } } /// A filter that includes all entities. public struct NoFilter: Filter { - public static func condition(for archetype: Archetype) -> Bool { + public typealias State = Void + public typealias Fetch = Void + + @inlinable + public static func _initState(world: World) -> Void { } + + @inlinable + public static func _initFetch(world: World, state: Void, lastTick: Tick, currentTick: Tick) -> Void { } + + @inlinable + public static func _setData(state: Void, fetch: Void, chunk: Chunk, archetype: Archetype) -> Void { } + + @inlinable + @inline(__always) + public static func condition(state: Void, fetch: Void, at row: Int) -> Bool { true } } diff --git a/Sources/AdaECS/Query/QueryPredicate.swift b/Sources/AdaECS/Query/QueryPredicate.swift index 2e174a6e8..fb97c4749 100644 --- a/Sources/AdaECS/Query/QueryPredicate.swift +++ b/Sources/AdaECS/Query/QueryPredicate.swift @@ -28,7 +28,7 @@ public extension QueryPredicate { /// - Returns: A new predicate that checks if the entity contains the given component. static func has(_ type: T.Type) -> QueryPredicate { QueryPredicate { archetype in - return archetype.componentsBitMask.contains(type.identifier) + return archetype.componentLayout.maskSet.contains(type.identifier) } } @@ -37,7 +37,7 @@ public extension QueryPredicate { /// - Returns: A new predicate that checks if the entity does not contain the given component. static func without(_ type: T.Type) -> QueryPredicate { QueryPredicate { archetype in - return !archetype.componentsBitMask.contains(type.identifier) + return !archetype.componentLayout.maskSet.contains(type.identifier) } } diff --git a/Sources/AdaECS/Query/QueryResult.swift b/Sources/AdaECS/Query/QueryResult.swift index 6410754de..3f42ef981 100644 --- a/Sources/AdaECS/Query/QueryResult.swift +++ b/Sources/AdaECS/Query/QueryResult.swift @@ -6,13 +6,13 @@ // /// Contains array of entities matched for the given EntityQuery request. -public struct QueryResult: Sequence, Sendable { +public struct QueryResult: Sequence, Sendable { /// The element type of the query result. public typealias Element = B.Components /// The iterator type of the query result. - public typealias Iterator = QueryTargetIterator + public typealias Iterator = FilterQueryIterator> /// The state of the query result. let state: QueryState @@ -36,38 +36,10 @@ public struct QueryResult: Sequence, Sendable { /// A Boolean value indicating whether the collection is empty. public var isEmpty: Bool { - return self.state.archetypes.isEmpty + return self.state.archetypeIndecies.isEmpty } public func makeIterator() -> Iterator { - QueryTargetIterator(state: self.state) + FilterQueryIterator>(state: state) } } - -/// An iterator that iterates over the query targets. -public struct QueryTargetIterator: IteratorProtocol { - - /// The element type of the query target iterator. - public typealias Element = B.Components - - /// The state of the query target iterator. - let state: QueryState - - /// The entity iterator of the query target iterator. - var entityIterator: EntityIterator - - /// Initialize a new query target iterator. - /// - Parameter state: The state of the query target iterator. - init(state: QueryState) { - self.entityIterator = .init(state: state) - self.state = state - } - - public mutating func next() -> Element? { - guard let entity = self.entityIterator.next() else { - return nil - } - - return B.getQueryTarget(from: entity) - } -} \ No newline at end of file diff --git a/Sources/AdaECS/Query/QueryTarget.swift b/Sources/AdaECS/Query/QueryTarget.swift index 979cebb18..fe3f81560 100644 --- a/Sources/AdaECS/Query/QueryTarget.swift +++ b/Sources/AdaECS/Query/QueryTarget.swift @@ -5,90 +5,299 @@ // Created by Vladislav Prusakov on 22.05.2025. // +import AdaUtils + +public protocol WorldQueryTarget: Sendable, ~Copyable { + associatedtype Fetch + + associatedtype State: Sendable + + @inlinable + static func _initState(world: World) -> State + + @inlinable + static func _initFetch( + world: World, + state: State, + lastTick: Tick, + currentTick: Tick + ) -> Fetch + + @inlinable + static func _setData( + state: State, + fetch: Fetch, + chunk: Chunk, + archetype: Archetype, + ) -> Fetch +} + /// A protocol that allows to use components and entities as query targets. -public protocol QueryTarget: Sendable, ~Copyable { +public protocol QueryTarget: WorldQueryTarget, ~Copyable { /// Check that entity contains target. /// - Parameter entity: The entity to check. /// - Returns: True if the entity contains the target, otherwise false. + @inlinable static func _queryTargetContains(in entity: Entity) -> Bool /// Create a new query target from an entity. /// - Parameter entity: The entity to create a query target from. /// - Returns: A new query target. - static func _queryTarget(from entity: Entity) -> Self + @inlinable + static func _queryFetch( + for entity: Entity, + state: State, + fetch: Fetch, + at row: Int + ) -> Self? /// Check if an archetype contains the target. /// - Parameter archetype: The archetype to check. /// - Returns: True if the archetype contains the target, otherwise false. - static func _queryContains(in archetype: Archetype) -> Bool + @inlinable + static func _queryContains(in archetype: borrowing Archetype) -> Bool } extension Component { + + @inlinable + public static func _initState(world: World) -> ComponentId { + return Self.identifier + } + + @inlinable + public static func _initFetch( + world: World, + state: ComponentId, + lastTick: Tick, + currentTick: Tick + ) -> ReadFetch { + ReadFetch(data: nil) + } + + public static func _queryFetch( + for entity: Entity, + state: ComponentId, + fetch: ReadFetch, + at row: Int + ) -> Self? { + unsafe fetch.data?[row] + } + + public static func _setData( + state: ComponentId, + fetch: ReadFetch, + chunk: Chunk, + archetype: Archetype + ) -> ReadFetch { + guard let slice = chunk.getComponentSlice(for: Self.self) else { + return fetch + } + return unsafe ReadFetch(data: slice) + } + + @inlinable public static func _queryTargetContains(in entity: Entity) -> Bool { return entity.components.has(Self.self) } - - public static func _queryTarget(from entity: Entity) -> Self { - return entity.components.get(by: Self.identifier)! + + @inlinable + public static func _queryContains(in archetype: borrowing Archetype) -> Bool { + archetype.componentLayout.maskSet.contains(Self.identifier) } - - public static func _queryContains(in archetype: Archetype) -> Bool { - archetype.componentsBitMask.contains(Self.identifier) +} + +@safe +public struct RefFetch { + @usableFromInline + var data: UnsafeMutableBufferPointer? + + @usableFromInline + var ticks: UnsafeMutableBufferPointer? + + @usableFromInline + var lastTick: Tick + @usableFromInline + var currentTick: Tick + + @usableFromInline + init( + data: UnsafeMutableBufferPointer?, + ticks: UnsafeMutableBufferPointer?, + lastTick: Tick, + currentTick: Tick + ) { + unsafe self.data = data + unsafe self.ticks = ticks + self.lastTick = lastTick + self.currentTick = currentTick + } +} + +@safe +public struct ReadFetch { + var data: UnsafeBufferPointer? + + @usableFromInline + init(data: UnsafeBufferPointer?) { + unsafe self.data = data } } +extension Ref: WorldQueryTarget where T: Component {} extension Ref: QueryTarget where T: Component { - + public static func _initFetch( + world: World, + state: ComponentId, + lastTick: Tick, + currentTick: Tick + ) -> RefFetch { + RefFetch( + data: nil, + ticks: nil, + lastTick: lastTick, + currentTick: currentTick + ) + } + + public static func _initState(world: World) -> ComponentId { + T.identifier + } + + @inlinable public static func _queryTargetContains(in entity: Entity) -> Bool { T._queryTargetContains(in: entity) } - - public static func _queryTarget(from entity: Entity) -> Ref { - Ref { - entity.components.get(T.self) - } set: { - entity.components.set($0) + + public static func _setData( + state: ComponentId, + fetch: RefFetch, + chunk: Chunk, + archetype: Archetype + ) -> RefFetch { + var newFetch = fetch + guard + let slice = chunk.getMutableComponentSlice(for: T.self), + let ticks = chunk.getMutableComponentTicksSlice(for: T.self) + else { + return fetch } + unsafe newFetch.data = UnsafeMutableBufferPointer( + start: slice, + count: chunk.count + ) + unsafe newFetch.ticks = UnsafeMutableBufferPointer( + start: ticks, + count: chunk.count + ) + return newFetch } - - public static func _queryContains(in archetype: Archetype) -> Bool { - return archetype.componentsBitMask.contains(T.identifier) + + public static func _queryFetch( + for entity: Entity, + state: ComponentId, + fetch: RefFetch, + at row: Int + ) -> Ref? { + return unsafe Ref( + pointer: unsafe fetch.data?.baseAddress?.advanced(by: row), + changeTick: ChangeDetectionTick( + change: fetch.ticks?.baseAddress?.advanced(by: row).unsafeBox(), + lastTick: fetch.lastTick, + currentTick: fetch.currentTick + ) + ) + } + + @inlinable + public static func _queryContains(in archetype: borrowing Archetype) -> Bool { + return archetype.componentLayout.maskSet.contains(T.identifier) } } extension Entity: QueryTarget { - + public static func _setData( + state: Void, + fetch: Void, + chunk: Chunk, + archetype: Archetype + ) -> Void { } + + public static func _queryFetch( + for entity: Entity, + state: (), + fetch: (), + at row: Int + ) -> Self? { + entity as? Self + } + + public static func _initState(world: World) -> Void { } + + public static func _initFetch( + world: World, + state: Void, + lastTick: Tick, + currentTick: Tick + ) -> Void { } + + @inlinable public static func _queryTargetContains(in entity: Entity) -> Bool { return true } - public static func _queryTarget(from entity: Entity) -> Self { - return entity as! Self - } - /// Always returns true because entity is always present in an archetype. - public static func _queryContains(in archetype: Archetype) -> Bool { + @inlinable + public static func _queryContains(in archetype: borrowing Archetype) -> Bool { return true } } +extension Optional: WorldQueryTarget where Wrapped: QueryTarget {} extension Optional: QueryTarget where Wrapped: QueryTarget { - + public typealias State = Wrapped.State + public typealias Fetch = Wrapped.Fetch + + @inlinable + public static func _setData( + state: Wrapped.State, + fetch: Wrapped.Fetch, + chunk: Chunk, + archetype: Archetype + ) -> Wrapped.Fetch { + Wrapped._setData(state: state, fetch: fetch, chunk: chunk, archetype: archetype) + } + + @inlinable + public static func _queryFetch( + for entity: Entity, + state: Wrapped.State, + fetch: Wrapped.Fetch, + at row: Int + ) -> Optional? { + .some( + Wrapped._queryFetch(for: entity, state: state, fetch: fetch, at: row) + ) + } + + @inlinable + public static func _initState(world: World) -> State { + Wrapped._initState(world: world) + } + + @inlinable + public static func _initFetch(world: World, state: State, lastTick: Tick, currentTick: Tick) -> Fetch { + Wrapped._initFetch(world: world, state: state, lastTick: lastTick, currentTick: currentTick) + } + + @inlinable public static func _queryTargetContains(in entity: Entity) -> Bool { Wrapped._queryTargetContains(in: entity) } - - public static func _queryTarget(from entity: Entity) -> Self { - if Wrapped._queryTargetContains(in: entity) { - return .some(Wrapped._queryTarget(from: entity)) - } - - return .none - } - - /// Always returns true because optional can be nil. - public static func _queryContains(in archetype: Archetype) -> Bool { - return true + + @inlinable + public static func _queryContains(in archetype: borrowing Archetype) -> Bool { + return Wrapped._queryContains(in: archetype) } } diff --git a/Sources/AdaECS/Query/Ref.swift b/Sources/AdaECS/Query/Ref.swift index 4cdcc867a..750f5d334 100644 --- a/Sources/AdaECS/Query/Ref.swift +++ b/Sources/AdaECS/Query/Ref.swift @@ -5,67 +5,50 @@ // Created by Vladislav Prusakov on 22.05.2025. // +import AdaUtils + /// A reference to a component. /// Used to mutate component values via ``Query``. @dynamicMemberLookup @propertyWrapper -public final class Ref: @unchecked Sendable { - - /// The getter of the reference. - public typealias Getter = () -> T - - /// The setter of the reference. - public typealias Setter = (T) -> Void +@safe +public struct Ref: Sendable, ChangeDetectionable { + private nonisolated(unsafe) let pointer: UnsafeMutablePointer? + public var changeTick: ChangeDetectionTick /// The wrapped value of the reference. + @inline(__always) public var wrappedValue: T { - get { - return getValue!() + _read { + unsafe assert(self.pointer != nil, "Value \(T.self) is not stored in world.") + yield unsafe self.pointer!.pointee } - set { - setValue?(newValue) + nonmutating _modify { + unsafe assert(self.pointer != nil, "Value \(T.self) is not stored in world.") + yield unsafe &self.pointer!.pointee + self.setChanged() } } - /// Initialize a new reference. - public init() { - self.getValue = nil - self.setValue = nil - } - - /// The getter of the reference. - var getValue: Getter? - - /// The setter of the reference. - let setValue: Setter? - /// Create a new reference to a component. /// - Parameters: /// - get: A closure that returns the component value. /// - set: A closure that sets the component value. - public init(get: @escaping Getter, set: @escaping Setter) { - self.getValue = get - self.setValue = set + public init( + pointer: UnsafeMutablePointer?, + changeTick: ChangeDetectionTick + ) { + unsafe self.pointer = pointer + self.changeTick = changeTick } + @inline(__always) public subscript(dynamicMember dynamicMember: WritableKeyPath) -> U { - get { - return self.wrappedValue[keyPath: dynamicMember] + _read { + yield self.wrappedValue[keyPath: dynamicMember] } - set { - self.wrappedValue[keyPath: dynamicMember] = newValue - } - } -} - -extension Ref: SystemQuery where T == World { - public convenience init(from world: World) { - fatalError() - } - - public func update(from world: World) { - getValue = { - world + nonmutating _modify { + yield &self.wrappedValue[keyPath: dynamicMember] } } } diff --git a/Sources/AdaECS/Query/ResQuery.swift b/Sources/AdaECS/Query/ResQuery.swift index 994828503..da721e080 100644 --- a/Sources/AdaECS/Query/ResQuery.swift +++ b/Sources/AdaECS/Query/ResQuery.swift @@ -5,16 +5,21 @@ // Created by Vladislav Prusakov on 22.05.2025. // -/// A property wrapper that allows you to query a resource in a system. +import AdaUtils + +/// A property wrapper that allows you to query a resource from a world. +@dynamicMemberLookup @propertyWrapper -public final class ResQuery: @unchecked Sendable { +public final class Res: @unchecked Sendable { /// The value of the query. private var _value: T? /// The wrapped value of the query. - public var wrappedValue: T? { - return _value + public var wrappedValue: T { + _read { + yield _value! + } } /// Initialize a new resource query. @@ -26,22 +31,84 @@ public final class ResQuery: @unchecked Sendable { /// - Parameter world: The world that will be used to initialize the query. public init(from world: World) { self._value = world.getResource(T.self) + .unwrap(message: "Resource \(T.self) not found in world. Make sure to call world.insertResource(_:) before using Res.") } /// Get the value of the query. /// - Returns: The value of the query. - public func callAsFunction() -> T? { - _value + public func callAsFunction() -> T { + _value! } + + public subscript(dynamicMember dynamicMember: KeyPath) -> U { + self.wrappedValue[keyPath: dynamicMember] + } + } -extension ResQuery: SystemQuery { +extension Res: SystemParameter { public func update(from world: World) { - let resource = world.getResource(T.self) - if resource == nil { - return + guard let resource = T.getFromWorld(world) else { + fatalError("Resource \(T.self) not found in world. Make sure to call world.insertResource(_:) before using Res.") } - self._value = resource! + self._value = resource + } +} + +extension Optional: Resource where Wrapped: Resource { + public static func getFromWorld(_ world: borrowing World) -> Optional? { + world.getResource(Wrapped.self) + } +} + +/// A property wrapper that allows you to query a mutable resource from a world. +@dynamicMemberLookup +@propertyWrapper +public final class ResMut: @unchecked Sendable { + + /// The value of the query. + private var _value: Ref? + + /// The wrapped value of the query. + public var wrappedValue: T { + _read { + yield self._value!.wrappedValue + } + _modify { + yield &self._value!.wrappedValue + } + } + + /// Initialize a new resource query. + public init() { + self._value = nil + } + + /// Initialize a new resource query. + /// - Parameter world: The world that will be used to initialize the query. + public init(from world: World) { + self._value = world.getRefResource(T.self) + } + + /// Get the value of the query. + /// - Returns: The value of the query. + public func callAsFunction() -> Ref? { + _value + } + + public subscript(dynamicMember dynamicMember: WritableKeyPath) -> U { + _read { + yield self.wrappedValue[keyPath: dynamicMember] + } + _modify { + yield &self.wrappedValue[keyPath: dynamicMember] + } + } +} + +extension ResMut: SystemParameter { + public func update(from world: World) { + self._value = world.getRefResource(T.self) } } diff --git a/Sources/AdaECS/Query/SystemQuery.swift b/Sources/AdaECS/Query/SystemQuery.swift index 14e3a48e8..da80937b9 100644 --- a/Sources/AdaECS/Query/SystemQuery.swift +++ b/Sources/AdaECS/Query/SystemQuery.swift @@ -5,8 +5,8 @@ // Created by v.prusakov on 5/21/25. // -/// A protocol for system queries. -public protocol SystemQuery: Sendable { +/// A protocol that describe a query for world from a system. +public protocol SystemParameter: Sendable { /// Initialize a new system query. /// - Parameter world: The world that will be used to initialize the query. @@ -16,10 +16,15 @@ public protocol SystemQuery: Sendable { /// - Parameter world: The world that will be used to update the query. /// Updates the query state with the given world. func update(from world: World) + + /// Notify query that world finish execution + func finish(_ world: World) } -public extension SystemQuery { +public extension SystemParameter { func update(from world: World) { fatalError("Query should be implemented") } + + func finish(_ world: World) { } } diff --git a/Sources/AdaECS/Scheduler.swift b/Sources/AdaECS/Scheduler.swift index 3fec944ca..39e502408 100644 --- a/Sources/AdaECS/Scheduler.swift +++ b/Sources/AdaECS/Scheduler.swift @@ -1,4 +1,5 @@ import AdaUtils +import Logging /// Represents a scheduler stage in the ECS update loop. public struct SchedulerName: Hashable, Equatable, RawRepresentable, CustomStringConvertible, Sendable { @@ -15,6 +16,12 @@ public struct SchedulerName: Hashable, Equatable, RawRepresentable, CustomString } } +extension SchedulerName: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + self.rawValue = value + } +} + /// Default schedulers. public extension SchedulerName { /// The pre-update scheduler. @@ -40,10 +47,11 @@ public extension SchedulerName { public final class Schedulers: @unchecked Sendable { private(set) var schedulerLabels: [SchedulerName] private var schedulers: [SchedulerName: Scheduler] + private let logger: Logger = Logger(label: "org.adaengine.ecs.schedulers") /// Initialize a new schedulers. /// - Parameter schedulers: The schedulers to initialize. - public init(_ schedulers: [SchedulerName]) { + public init(_ schedulers: [SchedulerName] = []) { self.schedulerLabels = schedulers self.schedulers = Dictionary( uniqueKeysWithValues: schedulerLabels.map { ($0, Scheduler(name: $0)) } @@ -71,7 +79,8 @@ public final class Schedulers: @unchecked Sendable { /// - Parameter after: The scheduler to insert after. public func insert(_ scheduler: Scheduler, after: SchedulerName) { if schedulers[scheduler.name] != nil { - fatalError("Already exists") + logger.error("Scheduler already exists") + return } if let idx = schedulerLabels.firstIndex(of: after) { @@ -93,7 +102,8 @@ public final class Schedulers: @unchecked Sendable { /// - Parameter before: The scheduler to insert before. public func insert(_ scheduler: Scheduler, before: SchedulerName) { if schedulers[scheduler.name] != nil { - fatalError("Already exists") + logger.error("Scheduler already exists") + return } if let idx = schedulerLabels.firstIndex(of: before) { @@ -109,6 +119,28 @@ public final class Schedulers: @unchecked Sendable { public func getScheduler(_ scheduler: SchedulerName) -> Scheduler? { self.schedulers[scheduler] } + + public func getScope( + for schedulerName: SchedulerName, + scopeBlock: (inout Scheduler) async -> Void + ) async { + if var scheduler = self.schedulers[schedulerName] { + await scopeBlock(&scheduler) + self.schedulers[schedulerName] = scheduler + } else { + logger.error("Scheduler \(schedulerName) not found") + } + } + + /// Add system to scheduler. If scheduler not exists, than we create it. + public func addSystem(_ system: T, for schedulerName: SchedulerName) { + if schedulers[schedulerName] == nil { + schedulerLabels.append(schedulerName) + } + self.schedulers[schedulerName, default: Scheduler(name: schedulerName)] + .systemGraph + .addSystem(system) + } } /// A resource that contains the delta time. @@ -123,6 +155,18 @@ public struct DeltaTime: Resource { } } +/// A resource that contains the delta time. +public struct FixedTime: Resource { + /// The delta time. + public let deltaTime: AdaUtils.TimeInterval + + /// Initialize a new delta time. + /// - Parameter deltaTime: The delta time. + public init(deltaTime: AdaUtils.TimeInterval) { + self.deltaTime = deltaTime + } +} + /// A resource that contains the order of the default scheduler. public struct DefaultSchedulerOrder: Resource { public let order: [SchedulerName] @@ -133,30 +177,28 @@ public struct DefaultSchedulerOrder: Resource { } /// A system that runs the default scheduler. -@System +@PlainSystem public struct DefaultSchedulerRunner: Sendable { - @ResQuery + @Res private var order: DefaultSchedulerOrder? - @LocalIsolated + @Local private var lastUpdate: LongTimeInterval = 0 public init(world: World) { } - public func update(context: inout UpdateContext) { + public func update(context: UpdateContext) async { let world = context.world - let deltaTime = context.deltaTime - context.taskGroup.addTask { - for scheduler in order?.order ?? [] { - await world.runScheduler(scheduler, deltaTime: deltaTime) - } + let order = order?.order ?? [] + for scheduler in order { + await world.runScheduler(scheduler) } } } /// A scheduler that runs systems in a specific order. -public final class Scheduler: @unchecked Sendable { +public struct Scheduler: Sendable { public typealias RunnerBlock = (any System) -> Void /// The name of the scheduler. @@ -166,57 +208,32 @@ public final class Scheduler: @unchecked Sendable { public var systemGraph: SystemsGraph = SystemsGraph() /// The graph executor of the scheduler. - let graphExecutor: SystemsGraphExecutor = SystemsGraphExecutor() - - let runnerSystemsBuilder: (World) -> any System - - /// The runner system of the scheduler. - var runnerSystem: (any System)? + var graphExecutor: any SystemsGraphExecutor = SingleThreadedSystemsGraphExecutor() /// The last update time of the scheduler. @LocalIsolated private var lastUpdate: LongTimeInterval = 0 - /// Initialize a new scheduler. - /// - Parameter name: The name of the scheduler. - /// - Parameter system: The system type to run. - public init(name: SchedulerName, system: T.Type) { - self.name = name - self.runnerSystemsBuilder = { T.init(world: $0) } - } - /// Initialize a new scheduler. /// - Parameter name: The name of the scheduler. public init(name: SchedulerName) { self.name = name - self.runnerSystemsBuilder = { DefaultSchedulerRunner.init(world: $0) } } /// Run the scheduler. /// - Parameter world: The world to run the scheduler on. - @MainActor - public func run(world: World) async { + public mutating func run(world: World) async { let now = Time.absolute let deltaTime = TimeInterval(max(0, now - self.lastUpdate)) self.lastUpdate = now - if self.runnerSystem == nil { - self.runnerSystem = self.runnerSystemsBuilder(world) + if self.systemGraph.isChanged { + self.systemGraph.linkSystems() + self.graphExecutor.initialize(self.systemGraph) } + let name = self.name world.insertResource(DeltaTime(deltaTime: deltaTime)) - if let runnerSystem = runnerSystem { - await withTaskGroup(of: Void.self) { @MainActor group in - var context = WorldUpdateContext( - world: world, - deltaTime: deltaTime, - scheduler: name, - taskGroup: group - ) - runnerSystem.queries.queries.forEach { $0.update(from: world) } - runnerSystem.update(context: &context) - _ = consume context - } - } + await graphExecutor.execute(systemGraph, world: world, scheduler: name) } } diff --git a/Sources/AdaECS/System/Executors/SingleThreadedSystemsGraphExecutor.swift b/Sources/AdaECS/System/Executors/SingleThreadedSystemsGraphExecutor.swift new file mode 100644 index 000000000..2169a7d49 --- /dev/null +++ b/Sources/AdaECS/System/Executors/SingleThreadedSystemsGraphExecutor.swift @@ -0,0 +1,58 @@ +// +// SingleThreadedSystemsGraphExecutor.swift +// AdaEngine +// + +import AdaUtils +import Collections + +/// The executor of the systems graph. +public struct SingleThreadedSystemsGraphExecutor: SystemsGraphExecutor { + + var systems: [any System] = [] + + /// Initialize a new systems graph executor. + public init() {} + + public mutating func initialize(_ graph: borrowing SystemsGraph) { + systems = graph.systems + } + + /// Execute the systems graph. + /// - Parameter graph: The systems graph to execute. + /// - Parameter world: The world to execute the systems graph in. + /// - Parameter deltaTime: The delta time to execute the systems graph with. + /// - Parameter scheduler: The scheduler to execute the systems graph on. + public mutating func execute( + _ graph: borrowing SystemsGraph, + world: World, + scheduler: SchedulerName + ) async { + for system in systems { + await executeSystem( + system: system, + world: world, + scheduler: scheduler + ) + world.flush() + } + } + + @concurrent + private func executeSystem( + system: any System, + world: World, + scheduler: SchedulerName + ) async { + system.queries.update(from: world) + await system.update( + context: WorldUpdateContext( + world: world, + scheduler: scheduler + ) + ) + // TODO: I don't like that sync point + await system.queries.finish(world) + } +} + diff --git a/Sources/AdaECS/System/Executors/SystemsGraphExecutor.swift b/Sources/AdaECS/System/Executors/SystemsGraphExecutor.swift new file mode 100644 index 000000000..e0e0c8653 --- /dev/null +++ b/Sources/AdaECS/System/Executors/SystemsGraphExecutor.swift @@ -0,0 +1,18 @@ +// +// SystemsGraphExecutor.swift +// AdaEngine +// + +import AdaUtils +import Collections + +protocol SystemsGraphExecutor: Sendable { + + mutating func initialize(_ graph: borrowing SystemsGraph) + + mutating func execute( + _ graph: borrowing SystemsGraph, + world: World, + scheduler: SchedulerName + ) async +} diff --git a/Sources/AdaECS/System/Executors/Untitled.swift b/Sources/AdaECS/System/Executors/Untitled.swift new file mode 100644 index 000000000..70cd1ba81 --- /dev/null +++ b/Sources/AdaECS/System/Executors/Untitled.swift @@ -0,0 +1,29 @@ +// +// Untitled.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 29.11.2025. +// + +import Collections + +struct MultithreadedGraphExecutor: SystemsGraphExecutor { + func initialize( + _ graph: borrowing SystemsGraph + ) { + + } + + func execute( + _ graph: borrowing SystemsGraph, + world: World, + scheduler: SchedulerName + ) async { + + } +} + +public struct SystemFilterAccess: Sendable { + public var access: BitSet + public var denied: BitSet +} diff --git a/Sources/AdaECS/System/System+Runtime.swift b/Sources/AdaECS/System/System+Runtime.swift index 2aea1672c..fbea5edcb 100644 --- a/Sources/AdaECS/System/System+Runtime.swift +++ b/Sources/AdaECS/System/System+Runtime.swift @@ -5,37 +5,17 @@ // Created by v.prusakov on 5/24/22. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif // We should register our systems in engine, because we should initiate them in memory // TODO: (Vlad) Add system list to editor and generate file with registred systems. extension System { - - /// Call this method to add system to engine. - /// When engine will initiate system from scene file, it will try to find - /// system in registred list. - /// Otherwise system will not be initialized. - @MainActor - public static func registerSystem() { - SystemStorage.register(self) - } - /// The Swift name of the system. static var swiftName: String { return String(reflecting: self) } } - -enum SystemStorage { - - nonisolated(unsafe) private static var registeredSystem: [String: System.Type] = [:] - - /// Return registred system or try to find it by NSClassFromString (works only for objc runtime) - static func getRegistredSystem(for name: String) -> System.Type? { - return self.registeredSystem[name] ?? (NSClassFromString(name) as? System.Type) - } - - static func register(_ system: T.Type) { - self.registeredSystem[T.swiftName] = system - } -} diff --git a/Sources/AdaECS/System/System.swift b/Sources/AdaECS/System/System.swift index ca2112cd8..83110ed58 100644 --- a/Sources/AdaECS/System/System.swift +++ b/Sources/AdaECS/System/System.swift @@ -8,34 +8,22 @@ import AdaUtils /// Contains information about current world update. -public struct WorldUpdateContext: @unchecked Sendable, ~Copyable { +public struct WorldUpdateContext: Sendable { /// The updating world. public let world: World - /// The number of seconds elapsed since the last update. - public let deltaTime: AdaUtils.TimeInterval - /// The scheduler that will be used to schedule tasks. public let scheduler: SchedulerName - /// Custom task group that will be executed when system did finish update block. - public var taskGroup: TaskGroup - /// Initialize a new world update context. /// - Parameter world: The world that will be updated. - /// - Parameter deltaTime: The delta time that will be used to update the world. /// - Parameter scheduler: The scheduler that will be used to schedule tasks. - /// - Parameter taskGroup: The task group that will be executed when the system did finish update block. init( world: World, - deltaTime: AdaUtils.TimeInterval, - scheduler: SchedulerName, - taskGroup: TaskGroup + scheduler: SchedulerName ) { self.world = world - self.deltaTime = deltaTime self.scheduler = scheduler - self.taskGroup = taskGroup } } @@ -51,22 +39,22 @@ public struct WorldUpdateContext: @unchecked Sendable, ~Copyable { /// Like example, let's create a movement system: /// /// ```swift -/// @System +/// @PlainSystem /// struct MovementSystem { /// /// // Configure the query to world. /// // We want to recieve entities with `PlayerComponent` and `Transform` -/// @EntityQuery(where: .has(Transform.self) && .has(PlayerComponent.self)) +/// @FilterQuery, With> /// private var query /// -/// init(world: World) {} +/// @Res +/// private var input /// -/// func update(context: inout UpdateContext) { -/// self.query.forEach { entity in -/// // Get transform component from entity -/// let transform = entity.components[Transform.self]! +/// init(world: World) {} /// -/// if Input.isKeyPressed(.space) { +/// func update(context: UpdateContext) { +/// self.query.forEach { transform in +/// if input.isKeyPressed(.space) { /// // Add 5 points for vertical direction /// // if space button pressed /// transform.position.y += 5 @@ -76,7 +64,7 @@ public struct WorldUpdateContext: @unchecked Sendable, ~Copyable { /// } /// /// ``` -public protocol System { +public protocol System: Sendable { typealias UpdateContext = WorldUpdateContext @@ -84,16 +72,15 @@ public protocol System { @preconcurrency init(world: World) /// Updates entities every frame. - func update(context: inout UpdateContext) + @concurrent + func update(context: UpdateContext) async /// An array of queries for this system. /// That needs to be updated queries results for this system. /// You can ignore this property if you don't use ``EntityQuery`` property wrapper or similar query property wrappers. - /// - Note: This property is automatically generated by ``System(dependencies:)`` or ``PlainSystem(dependencies:)`` macro. Macro grab all properties with `Query` suffix and comfirmed ``SystemQuery`` protocol, example: `@CustomQuery`. + /// - Note: This property is automatically generated by ``System(dependencies:)`` or ``PlainSystem(dependencies:)`` macro. Macro grab all properties with `Query` suffix and confirmed ``SystemParameter`` protocol, example: `@CustomQuery`. var queries: SystemQueries { get } - // MARK: Dependencies - /// An array of dependencies for this system. static var dependencies: [SystemDependency] { get } } @@ -109,20 +96,42 @@ public extension System { } /// A collection of queries for a system. -public struct SystemQueries { - public let queries: [SystemQuery] +public struct SystemQueries: Sendable, Equatable { + + public let queries: [any SystemParameter] /// Initialize a new system queries. /// - Parameter queries: The queries to update. - public init(queries: [SystemQuery]) { + public init(queries: consuming [any SystemParameter]) { self.queries = queries } /// Update the queries from the world. /// - Parameter world: The world to update the queries from. public func update(from world: World) { + let world = world for query in queries { query.update(from: world) } } + + @WorldActor + public func finish(_ world: World) { + for query in queries { + query.finish(world) + } + } + + public static func == (lhs: SystemQueries, rhs: SystemQueries) -> Bool { + for lhsItem in lhs.queries { + let lhsItemType = type(of: lhsItem) + if !rhs.queries.contains(where: { + type(of: $0) == lhsItemType + }) { + return false + } + } + + return true + } } diff --git a/Sources/AdaECS/System/SystemDependency.swift b/Sources/AdaECS/System/SystemDependency.swift index af9966ed0..8d82645c0 100644 --- a/Sources/AdaECS/System/SystemDependency.swift +++ b/Sources/AdaECS/System/SystemDependency.swift @@ -10,7 +10,7 @@ /// If you need to specify the update order between your system and other systems in your app, you can do that using this property. /// /// ```swift -/// @System(dependencies: [ +/// @PlainSystem(dependencies: [ /// .after(EnemyChasingSystem.self), // Run MovementSystem after EnemyChasingSystem /// .before(BulletSystem.self) // Run MovementSystem before BulletSystem /// ]) diff --git a/Sources/AdaECS/System/SystemsGraph.swift b/Sources/AdaECS/System/SystemsGraph.swift index 4f1a431fb..c8d55c595 100644 --- a/Sources/AdaECS/System/SystemsGraph.swift +++ b/Sources/AdaECS/System/SystemsGraph.swift @@ -8,34 +8,37 @@ import OrderedCollections /// Contains information about execution order of systems. -public struct SystemsGraph: Sendable, ~Copyable { +public struct SystemsGraph: Sendable { + + /// Indicates that graph is changed and needs recalculate deps + private(set) var isChanged: Bool = false /// The edge of the systems graph. struct Edge: Equatable { /// The output node of the edge. let outputNode: String - + /// The input node of the edge. let inputNode: String } /// The node of the systems graph. - struct Node: @unchecked Sendable { + struct Node: Sendable { /// The unique identifier of the node. typealias ID = String /// The name of the node. let name: String - + /// The system of the node. var system: System - + /// The dependencies of the node. var dependencies: [SystemDependency] /// The input edges of the node. var inputEdges: [Edge] = [] - + /// The output edges of the node. var outputEdges: [Edge] = [] } @@ -47,10 +50,10 @@ public struct SystemsGraph: Sendable, ~Copyable { /// The nodes of the graph. private(set) var nodes: OrderedDictionary = [:] - + /// Initialize a new systems graph. public init() { } - + // MARK: - Internal methods /// Add a node of the current system. If a node exists with the same type, it will be overridden. @@ -59,6 +62,7 @@ public struct SystemsGraph: Sendable, ~Copyable { mutating func addSystem(_ system: T) { let node = Node(name: T.swiftName, system: system, dependencies: T.dependencies) self.nodes[node.name] = node + self.isChanged = true } /// Create an execution order for all systems. @@ -76,6 +80,7 @@ public struct SystemsGraph: Sendable, ~Copyable { } } } + self.isChanged = false } /// Get the output nodes for a given node. @@ -91,7 +96,6 @@ public struct SystemsGraph: Sendable, ~Copyable { guard let node = self.nodes[edge.inputNode] else { return nil } - return node } } @@ -125,7 +129,7 @@ public struct SystemsGraph: Sendable, ~Copyable { assert(outputNode != nil, "[SystemsGraph] System not exists \(outputSystemName) to \(inputSystemName)") assert(inputNode != nil, "[SystemsGraph] System not exists \(inputSystemName) for \(outputSystemName)") - + let edge = Edge(outputNode: outputSystemName, inputNode: inputSystemName) let reversedEdge = Edge(outputNode: inputSystemName, inputNode: outputSystemName) @@ -157,13 +161,15 @@ public struct SystemsGraph: Sendable, ~Copyable { /// - Parameter edge: The edge to check. /// - Returns: True if the edge exists, otherwise false. private func hasEdge(_ edge: Edge) -> Bool { - guard let inputNode = self.nodes[edge.inputNode], let outputNode = self.nodes[edge.outputNode] else { + guard + let inputNode = self.nodes[edge.inputNode], + let outputNode = self.nodes[edge.outputNode] + else { return false } return inputNode.inputEdges.firstIndex(of: edge) != nil && outputNode.outputEdges.firstIndex(of: edge) != nil } - } extension SystemsGraph { diff --git a/Sources/AdaECS/System/SystemsGraphExecutor.swift b/Sources/AdaECS/System/SystemsGraphExecutor.swift deleted file mode 100644 index 2f9127913..000000000 --- a/Sources/AdaECS/System/SystemsGraphExecutor.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// SystemsGraphExecutor.swift -// AdaEngine -// -// Created by v.prusakov on 2/24/23. -// - -import AdaUtils -import Collections - -// TOOD: Parallel execution for non dependent values - -/// The executor of the systems graph. -public struct SystemsGraphExecutor: Sendable { - - /// Initialize a new systems graph executor. - public init() {} - - /// Execute the systems graph. - /// - Parameter graph: The systems graph to execute. - /// - Parameter world: The world to execute the systems graph in. - /// - Parameter deltaTime: The delta time to execute the systems graph with. - /// - Parameter scheduler: The scheduler to execute the systems graph on. - public func execute( - _ graph: borrowing SystemsGraph, - world: World, - deltaTime: AdaUtils.TimeInterval, - scheduler: SchedulerName - ) async { - var completedSystems: Set = [] - completedSystems.reserveCapacity(graph.nodes.count) - - let values = graph.nodes.values.elements.filter { $0.inputEdges.isEmpty } - var nodes: Deque = Deque(values) - - nextNode: - while let currentNode = nodes.popLast() { - // if we has a outputs for node we should skip it - if completedSystems.contains(currentNode.name) { - continue - } - - for inputNode in graph.getInputNodes(for: currentNode.name) { - if !completedSystems.contains(inputNode.name) { - nodes.prepend(currentNode) - continue nextNode - } - } - - currentNode.system.queries.update(from: world) - - await withTaskGroup(of: Void.self) { @MainActor group in - var context = WorldUpdateContext( - world: world, - deltaTime: deltaTime, - scheduler: scheduler, - taskGroup: group - ) - - currentNode.system.update(context: &context) - _ = consume context - } - world.flush() - completedSystems.insert(currentNode.name) - - for outputNode in graph.getOuputNodes(for: currentNode.name) { - nodes.prepend(outputNode) - } - } - } -} diff --git a/Sources/AdaECS/World/Archetype.swift b/Sources/AdaECS/World/Archetype.swift index 34d5ae27c..ac8880e02 100644 --- a/Sources/AdaECS/World/Archetype.swift +++ b/Sources/AdaECS/World/Archetype.swift @@ -6,40 +6,170 @@ // import AdaUtils +import Atomics +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif /// The unique identifier of the component. +@frozen public struct ComponentId: Hashable, Equatable, Sendable { /// The unique identifier of the component. + @usableFromInline let id: Int } -/// The record of the entity. -struct EntityRecord: Sendable { - /// The unique identifier of the archetype that contains the entity. - var archetypeId: Archetype.ID - - /// The index of the entity in the archetype. - var row: Int +public struct EntityLocation: Sendable, Hashable { + public let archetypeId: Archetype.ID + public let archetypeRow: Int + public let chunkIndex: Int + public let chunkRow: Int +} + +public struct ArchetypeSwapAndRemoveResult: Sendable { + public let swappedEntity: Entity.ID? + public let entityRow: Int +} + +public final class Entities: @unchecked Sendable { + public package(set) var entities: SparseSet = [:] + private let currentId = ManagedAtomic(1) + + func allocate(with name: String) -> Entity { + let newId = currentId.loadThenWrappingIncrement(ordering: .relaxed) + return Entity(name: name, id: newId) + } + + func addNotAllocatedEntity(_ entity: Entity) { + guard entity.id == Entity.notAllocatedId else { return } + let newId = currentId.loadThenWrappingIncrement(ordering: .relaxed) + entity.id = newId + entity.components.entity = newId + } + + func clear() { + currentId.store(1, ordering: .relaxed) + entities.removeAll(keepingCapacity: true) + } +} + +public final class Archetypes: @unchecked Sendable { + public var componentsIndex: [ComponentMaskSet: Archetype.ID] + public var archetypes: ContiguousArray + + public init( + componentsIndex: [ComponentMaskSet: Archetype.ID] = [:], + archetypes: ContiguousArray = [] + ) { + let emptyArchetype = Archetype.new(index: 0, componentLayout: ComponentLayout(components: [])) + self.componentsIndex = [ComponentMaskSet(): emptyArchetype.id] + self.archetypes = [emptyArchetype] + } + + public func getOrCreate(for componentLayout: ComponentLayout) -> Archetype.ID { + if let archetypeIndex = self.componentsIndex[componentLayout.maskSet] { + return archetypeIndex + } + + let newIndex = archetypes.count + let archetype = Archetype.new(index: newIndex, componentLayout: componentLayout) + self.archetypes.append(archetype) + componentsIndex[componentLayout.maskSet] = newIndex + return newIndex + } + + public func clear() { + for index in 0..(components: repeat each T) { + var components = [any Component.Type]() + var maskSet = ComponentMaskSet() + for component in repeat (each T).self { + let id = component.identifier + components.append(component) + maskSet.insert(id) + } + self.components = components + self.maskSet = maskSet + } + + public mutating func insert(_ component: T.Type) { + self.maskSet.insert(component) + self.components.append(component) + } + + public mutating func insert(_ component: any Component.Type) { + self.maskSet.insert(component) + self.components.append(component) + } + + public mutating func remove(_ component: ComponentId) { + self.maskSet.remove(component) + self.components.removeAll { $0.identifier == component } + } + + public static func == (lhs: ComponentLayout, rhs: ComponentLayout) -> Bool { + lhs.maskSet == rhs.maskSet + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.maskSet) + } } /// Types for defining Archetypes, collections of entities that have the same set of /// components. -public struct Archetype: Hashable, Identifiable, Sendable { +public struct Archetype: Identifiable, Sendable { /// The unique identifier of the archetype. public let id: Int - /// The entities in the archetype. - public internal(set) var entities: SparseArray = [] + public internal(set) var chunks: Chunks - /// The fried entities in the archetype. - @usableFromInline - private(set) var friedEntities: [Int] = [] + /// The entities in the archetype. + public internal(set) var entities: ContiguousArray = [] /// The edge of the archetype. - var edge: Edge = Edge() + @usableFromInline + var edges: Edges = Edges() /// The components bit mask of the archetype. - public internal(set) var componentsBitMask: BitSet = BitSet() + public internal(set) var componentLayout: ComponentLayout /// Initialize a new archetype. /// - Parameter id: The unique identifier of the archetype. @@ -47,78 +177,81 @@ public struct Archetype: Hashable, Identifiable, Sendable { private init( id: Archetype.ID, entities: [Entity] = [], - componentsBitMask: BitSet = BitSet() + componentLayout: ComponentLayout ) { self.id = id - self.entities = SparseArray(entities) - self.componentsBitMask = componentsBitMask - self.friedEntities.reserveCapacity(30) + self.entities = ContiguousArray(entities) + self.componentLayout = componentLayout + self.chunks = Chunks(componentLayout: componentLayout) } - +} + +public extension Archetype { + + /// Checks if the archetype has any entities. + var isEmpty: Bool { + self.entities.isEmpty + } + /// Create a new archetype. /// - Parameter index: The index of the archetype. /// - Returns: A new archetype. @inline(__always) - static func new(index: Int) -> Archetype { - return Archetype(id: index) + static func new(index: Int, componentLayout: ComponentLayout) -> Archetype { + return Archetype(id: index, componentLayout: componentLayout) } - + /// Append an entity to the archetype. /// - Parameter entity: The entity to append. /// - Returns: The record of the entity. @inline(__always) - mutating func append(_ entity: Entity) -> EntityRecord { - let row: Int - - if !friedEntities.isEmpty { - let index = self.friedEntities.removeLast() - self.entities.insert(entity, at: index) - row = index - } else { - self.entities.append(entity) - row = self.entities.count - 1 - } - - return EntityRecord( - archetypeId: self.id, - row: row - ) + mutating func append(_ entity: consuming Entity) -> Int { + self.entities.append(entity) + return self.entities.count - 1 } - + /// Remove an entity from the archetype. /// - Parameter index: The index of the entity to remove. + @discardableResult @inline(__always) - mutating func remove(at index: Int) { - self.entities.remove(at: index) - self.friedEntities.append(index) + mutating func swapRemove(at index: Int) -> ArchetypeSwapAndRemoveResult { + let isLast = index == self.entities.count - 1 + _ = self.entities.swapRemove(at: index) + + return ArchetypeSwapAndRemoveResult( + swappedEntity: isLast ? nil : self.entities[index].id, + entityRow: index + ) } - + /// Clear the archetype. @inline(__always) mutating func clear() { - self.componentsBitMask = BitSet() - self.friedEntities.removeAll() + self.chunks.clear() self.entities.removeAll() - self.edge = Edge() + self.edges = Edges() } - - // MARK: - Hashable - +} + +// MARK: - Hashable + +extension Archetype: Hashable { + /// Hash the archetype. /// - Parameter hasher: The hasher to hash the archetype. public func hash(into hasher: inout Hasher) { hasher.combine(id) - hasher.combine(componentsBitMask) + hasher.combine(componentLayout) hasher.combine(entities) } - + /// Check if two archetypes are equal. /// - Parameter lhs: The left archetype. /// - Parameter rhs: The right archetype. /// - Returns: True if the two archetypes are equal, otherwise false. public static func == (lhs: Archetype, rhs: Archetype) -> Bool { return lhs.entities == rhs.entities && - lhs.id == rhs.id && lhs.componentsBitMask == rhs.componentsBitMask + lhs.id == rhs.id && lhs.componentLayout == rhs.componentLayout } } @@ -129,54 +262,105 @@ extension Archetype: CustomStringConvertible { Archetype( id: \(id) entityIds: \(entities.compactMap { $0.id }) - componentsBitMask: \(componentsBitMask) + componentsLayout: \(componentLayout) ) """ } } extension Archetype { - /// The edge of the archetype. - struct Edge: Hashable, Equatable, Sendable { + /// The edges of the archetype. + @usableFromInline + struct Edges: Hashable, Sendable { /// The components to add. - var add: [ComponentId : Archetype] = [:] + private var add: [ComponentLayout: Archetype.ID] = [:] /// The components to remove. - var remove: [ComponentId : Archetype] = [:] + private var remove: [ComponentLayout: Archetype.ID] = [:] + + @inline(__always) + mutating func addArchetypeAfterInsertion( + _ archetype: Archetype.ID, + for layout: ComponentLayout + ) { + self.add[layout] = archetype + } + + @inline(__always) + mutating func addArchetypeAfterRemoval( + _ archetype: Archetype.ID, + for layout: ComponentLayout + ) { + self.remove[layout] = archetype + } + + @inline(__always) + mutating func getArchetypeAfterInsertion( + for layout: ComponentLayout + ) -> Archetype.ID? { + self.add[layout] + } + + @inline(__always) + mutating func getArchetypeAfterRemoval( + for layout: ComponentLayout + ) -> Archetype.ID? { + self.remove[layout] + } } } -//// FIXME: (Vlad) not a bit set! -public struct BitSet: Equatable, Hashable, Sendable { - // TODO: (Vlad) Not efficient in memory layout. - private var mask: Set +public struct ComponentMaskSet: Hashable, Sendable { + @usableFromInline + var mask: Set var isEmpty: Bool { return self.mask.isEmpty } + @usableFromInline init(reservingCapacity: Int = 0) { self.mask = [] self.mask.reserveCapacity(reservingCapacity) } + @inlinable mutating func insert(_ component: T.Type) { self.mask.insert(T.identifier) } - mutating func insert(_ component: ComponentId) { + @inlinable + mutating func insert(_ component: consuming ComponentId) { self.mask.insert(component) } + @inlinable mutating func remove(_ component: T.Type) { self.mask.remove(T.identifier) } - public func contains(_ identifier: ComponentId) -> Bool { + @inlinable + mutating func remove(_ componentId: ComponentId) { + self.mask.remove(componentId) + } + + @inlinable + public func contains(_ identifier: consuming ComponentId) -> Bool { self.mask.contains(identifier) } + @inlinable func contains(_ component: T.Type) -> Bool { return self.mask.contains(T.identifier) } } + +extension Array where Element == Component { + var maskSet: ComponentMaskSet { + var set = ComponentMaskSet(reservingCapacity: self.count) + for component in self { + set.insert(type(of: component).identifier) + } + return set + } +} diff --git a/Sources/AdaECS/World/BlobArray.swift b/Sources/AdaECS/World/BlobArray.swift new file mode 100644 index 000000000..213128f30 --- /dev/null +++ b/Sources/AdaECS/World/BlobArray.swift @@ -0,0 +1,192 @@ +// +// BlobArray.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 10.06.2025. +// + +import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@safe +public struct BlobArray: Sendable { + @unsafe + final class _Buffer: @unchecked Sendable { + let count: Int + let pointer: UnsafeMutableRawBufferPointer + var deinitializer: ((UnsafeMutableRawBufferPointer, Int) -> Void)? + + init( + count: Int, + pointer: UnsafeMutableRawBufferPointer, + deinitializer: ((UnsafeMutableRawBufferPointer, Int) -> Void)? = nil + ) { + unsafe self.count = count + unsafe self.pointer = pointer + unsafe self.deinitializer = deinitializer + } + + deinit { + unsafe pointer.deallocate() + } + + func clear(_ count: Int) { + unsafe deinitializer?(pointer, count) + } + } + + public struct ElementLayout: Sendable { + public let size: Int + public let alignment: Int + + public init(size: Int, alignment: Int) { + self.size = size + self.alignment = alignment + } + } + + var buffer: _Buffer + public let layout: ElementLayout + public private(set) var count: Int + let label: String? + + public init( + count: Int, + of type: T.Type, + deinitializer: ((UnsafeMutableRawBufferPointer, Int) -> Void)? = nil + ) { + self.count = count + self.layout = ElementLayout(size: MemoryLayout.stride, alignment: MemoryLayout.alignment) + unsafe self.buffer = _Buffer( + count: count, + pointer: .allocate( + byteCount: count * MemoryLayout.stride, + alignment: MemoryLayout.alignment + ), + deinitializer: deinitializer + ) + self.label = String(describing: T.self) + } +} + +public extension BlobArray { + mutating func realloc(_ count: Int) { + let newBuffer = unsafe _Buffer( + count: count, + pointer: .allocate( + byteCount: count * self.layout.size, + alignment: self.layout.alignment + ), + deinitializer: buffer.deinitializer + ) + unsafe newBuffer.pointer.copyMemory(from: UnsafeRawBufferPointer(self.buffer.pointer)) + unsafe self.buffer = newBuffer + self.count = count + } + + func clear(_ count: Int) { + unsafe self.buffer.clear(count) + } + + func insert(_ element: consuming T, at index: Int) { + #if DEBUG + precondition( + MemoryLayout.stride == self.layout.size && + MemoryLayout.alignment == self.layout.alignment, + "Element has different layout" + ) + #endif + unsafe self.buffer.pointer + .baseAddress! + .advanced(by: index * self.layout.size) + .assumingMemoryBound(to: T.self) + .initialize(to: element) + } + + func getMutablePointer(at index: Int, as type: T.Type) -> UnsafeMutablePointer { + #if DEBUG + precondition( + MemoryLayout.stride == self.layout.size && + MemoryLayout.alignment == self.layout.alignment, + "Element has different layout" + ) + #endif + + return unsafe self.buffer.pointer.baseAddress! + .advanced(by: index * self.layout.size) + .bindMemory(to: type, capacity: self.layout.size) + } + + func get(at index: Int, as type: T.Type) -> T { + #if DEBUG + precondition( + MemoryLayout.stride == self.layout.size && + MemoryLayout.alignment == self.layout.alignment, + "Element has different layout" + ) + #endif + return unsafe self.buffer.pointer.baseAddress! + .advanced(by: index * self.layout.size) + .bindMemory(to: type, capacity: self.layout.size) + .pointee + } + + func swap( + from fromIndex: Int, + to toIndex: Int + ) { + precondition(fromIndex >= 0 && toIndex >= 0) + precondition(layout.size >= 0) + + if fromIndex == toIndex || layout.size == 0 { + return + } + let base = unsafe buffer.pointer.baseAddress! + let fromPointer = unsafe base.advanced(by: fromIndex * layout.size) + let toPointer = unsafe base.advanced(by: toIndex * layout.size) + + unsafe withUnsafeTemporaryAllocation(of: UInt8.self, capacity: layout.size) { tmp in + let tempPointer = UnsafeMutableRawPointer(tmp.baseAddress!) + unsafe tempPointer.copyMemory(from: fromPointer, byteCount: layout.size) + unsafe fromPointer.copyMemory(from: toPointer, byteCount: layout.size) + unsafe toPointer.copyMemory(from: tempPointer, byteCount: layout.size) + } + } + + func remove(at index: Int) -> T { + #if DEBUG + precondition( + MemoryLayout.stride == self.layout.size && + MemoryLayout.alignment == self.layout.alignment, + "Element has different layout" + ) + #endif + let pointer = unsafe self.buffer.pointer.baseAddress! + .advanced(by: index * self.layout.size) + .assumingMemoryBound(to: T.self) + let element = unsafe pointer.pointee + unsafe pointer.deinitialize(count: 1) + return element + } + + func copyElement( + to blobArray: inout BlobArray, + from fromIndex: Int, + to toIndex: Int + ) { + #if DEBUG + precondition( + self.layout.size == blobArray.layout.size && + self.layout.alignment == blobArray.layout.alignment, + "BlobArray has different layout" + ) + #endif + let sourcePointer = unsafe self.buffer.pointer.baseAddress!.advanced(by: fromIndex * self.layout.size) + let destinationPointer = unsafe blobArray.buffer.pointer.baseAddress!.advanced(by: toIndex * self.layout.size) + unsafe destinationPointer.copyMemory(from: sourcePointer, byteCount: self.layout.size) + } +} diff --git a/Sources/AdaECS/World/Chunks.swift b/Sources/AdaECS/World/Chunks.swift new file mode 100644 index 000000000..9967487f3 --- /dev/null +++ b/Sources/AdaECS/World/Chunks.swift @@ -0,0 +1,507 @@ +// +// Chunks.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 07.06.2025. +// + +import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import OrderedCollections + +// TODO: A lot of unsafe code. What we can do? Use Span? + +public struct ChunkLocation: Sendable { + public let chunkIndex: Int + public let entityRow: Int +} + +public struct MoveEntityResult: Sendable { + public let newLocation: ChunkLocation + public let swappedEntity: Entity.ID? +} + +/// A chunk-based storage system for ECS components +/// Provides memory-efficient, cache-friendly storage for entities and their components +public struct Chunks: Sendable { + + /// Array of chunks for different archetypes + public internal(set) var chunks: ContiguousArray = [] + + /// Configuration for chunk storage + public let entitiesPerChunk: Int + + private var friedLocation: [ChunkLocation] = [] + + /// Location entity in chunk + public private(set) var entities: SparseSet = [:] + + let componentLayout: ComponentLayout + + public var count: Int { + chunks.count + } + + public init(entitiesPerChunk: Int = 250, componentLayout: ComponentLayout) { + self.entitiesPerChunk = entitiesPerChunk + self.componentLayout = componentLayout + self.chunks.append(Chunk(entitiesPerChunk: entitiesPerChunk, layout: componentLayout)) + } +} + +public extension Chunks { + mutating func getFreeChunkIndex() -> Int { + if let firstLocation = friedLocation.popLast() { + return firstLocation.chunkIndex + } else if let possibleIndex = chunks.firstIndex(where: { !$0.isFull }) { + return possibleIndex + } else { + let chunk = Chunk(entitiesPerChunk: entitiesPerChunk, layout: componentLayout) + self.chunks.append(chunk) + return self.chunks.endIndex - 1 + } + } + + func insert( + _ component: T, + for entity: Entity.ID, + lastTick: Tick + ) { + guard let location = self.entities[entity] else { + return + } + self.chunks[location.chunkIndex] + .insert( + component, + at: location.chunkIndex, + lastTick: lastTick + ) + } + + subscript(_ index: Int) -> Chunk { + _read { yield chunks[index] } + _modify { yield &chunks[index] } + } + + @discardableResult + mutating func insertEntity( + _ entity: Entity.ID, + components: [any Component], + tick: Tick + ) -> ChunkLocation { + let location = self.getFreeChunkIndex() + var chunk = self.chunks[location] + guard let entityLocation = chunk.addEntity(entity) else { + fatalError("Failed to add entity \(entity) to chunk \(location)") + } + chunk.insert(at: entityLocation, components: components, tick: tick) + let chunkLocation = ChunkLocation( + chunkIndex: location, + entityRow: entityLocation + ) + self.entities[entity] = chunkLocation + self.chunks[location] = chunk + return chunkLocation + } + + @discardableResult + mutating func removeEntity(_ entity: Entity.ID) -> MoveEntityResult? { + guard let location = self.entities[entity] else { + return nil + } + var chunk = self.chunks[location.chunkIndex] + let swappedEntity = chunk.swapRemoveEntity(at: entity) + self.chunks[location.chunkIndex] = chunk + self.friedLocation.append(location) + return MoveEntityResult(newLocation: location, swappedEntity: swappedEntity) + } + + mutating func moveEntity(_ entity: Entity.ID, to chunks: inout Chunks) -> MoveEntityResult { + guard let location = self.entities[entity] else { + fatalError("Entity \(entity) not found in chunks") + } + let oldChunk = self.chunks[location.chunkIndex] + let newLocation = chunks.getFreeChunkIndex() + var chunk = chunks.chunks[newLocation] + let entityLocation = chunk.addEntity(entity)! + let chunkLocation = ChunkLocation( + chunkIndex: newLocation, + entityRow: entityLocation + ) + chunks.entities[entity] = chunkLocation + for component in chunks.componentLayout.components { + guard + let oldChunkComponent = oldChunk.componentsData[component.identifier], + var newChunkComponent = chunk.componentsData[component.identifier] + else { + continue + } + + oldChunkComponent.data + .copyElement( + to: &newChunkComponent.data, + from: location.entityRow, + to: chunkLocation.entityRow + ) + oldChunkComponent.changesTicks + .copyElement( + to: &newChunkComponent.changesTicks, + from: location.entityRow, + to: chunkLocation.entityRow + ) + chunk.componentsData[component.identifier] = newChunkComponent + } + chunks.chunks[newLocation] = chunk + let swappedEntity = self.swapRemoveEntity(entity) + return MoveEntityResult(newLocation: chunkLocation, swappedEntity: swappedEntity) + } + + @discardableResult + private mutating func swapRemoveEntity(_ entity: Entity.ID) -> Entity.ID? { + guard let location = self.entities[entity] else { + return nil + } + + var chunk = self.chunks[location.chunkIndex] + let swappedEntityId = chunk.swapRemoveEntity(at: entity) + self.entities.remove(for: entity) + + if let swappedEntityId = swappedEntityId { + self.entities[swappedEntityId] = location + } + self.chunks[location.chunkIndex] = chunk + + return swappedEntityId + } + + mutating func clear() { + for index in 0..(for type: T.Type) -> [UnsafeBufferPointer] { + var slices: [UnsafeBufferPointer] = unsafe [] + for index in 0..( + for type: T.Type + ) -> [UnsafeBufferPointer] { + var slices: [UnsafeBufferPointer] = unsafe [] + for index in 0..(capacity: Int, component: T.Type) { + self.data = unsafe BlobArray(count: capacity, of: T.self) { pointer, count in + if !component.componentsInfo.isPlainOldData { + unsafe pointer.assumingMemoryBound(to: T.self) + .baseAddress? + .deinitialize(count: count) + } + } + self.changesTicks = BlobArray(count: capacity, of: Tick.self) + self.componentType = component + } + + public var description: String { + return """ + ComponentsData( + data: \(data.count), + changesTicks: \(changesTicks.count), + componentType: \(componentType) + ) + """ + } + } + + @unsafe + public struct ComponentData { + public let component: UnsafeMutablePointer + public let changeTick: Tick + } + + /// Maximum number of entities this chunk can hold + public let entitiesPerChunk: Int + + /// Current number of entities in this chunk + public private(set) var count: Int = 0 + + /// Entity IDs stored in this chunk + public private(set) var entities: ContiguousArray + + /// Map from entity ID to its index in the chunk + public private(set) var entityIndices: [Entity.ID: RowIndex] + + /// Raw component data storage (organized by component type) + public internal(set) var componentsData: SparseSet + + var currentIndex: Int { + self.count + } + + public var isEmpty: Bool { + self.count == 0 + } + + public var isFull: Bool { + self.count == entitiesPerChunk + } + + public init(entitiesPerChunk: Int, layout: ComponentLayout) { + self.entitiesPerChunk = entitiesPerChunk + self.entities = [] + self.entityIndices = [:] + self.entityIndices.reserveCapacity(entitiesPerChunk) + self.componentsData = [:] + for component in layout.components { + self.componentsData[component.identifier] = ComponentsData( + capacity: entitiesPerChunk * MemoryLayout.stride(ofValue: component), + component: component + ) + } + } + + /// Add an entity to this chunk + /// - Parameter entityId: The entity identifier to add + /// - Returns: The index where the entity was placed, or nil if chunk is full + mutating func addEntity(_ entityId: Entity.ID) -> RowIndex? { + if count >= entitiesPerChunk { + assertionFailure("Chunk is full, cannot add entity \(entityId), self \(self.description)") + return nil + } + + if currentIndex < self.entities.count { + self.entities[currentIndex] = entityId + } else { + self.entities.append(entityId) + } + + let index = currentIndex + entityIndices[entityId] = index + count += 1 + + return index + } + + /// Remove an entity from this chunk + /// - Parameter index: The index of the entity to remove + mutating func removeEntity(at entityId: Entity.ID) { + self.swapRemoveEntity(at: entityId) + } + + mutating func clear() { + self.componentsData.forEach { data in + data.data.clear(entities.count) + data.changesTicks.clear(entities.count) + } + self.entities.removeAll(keepingCapacity: true) + self.entityIndices.removeAll(keepingCapacity: true) + self.count = 0 + } + + /// Removes an entity from the chunk by swapping it with the last element. + /// - Parameter entityId: The ID of the entity to remove. + /// - Returns: The ID of the entity that was swapped into the removed entity's place, if any. + @discardableResult + mutating func swapRemoveEntity(at entityId: Entity.ID) -> Entity.ID? { + guard let removedIndex = self.entityIndices.removeValue(forKey: entityId) else { + return nil + } + + count -= 1 + let lastIndex = count + + if removedIndex < lastIndex { + // Move component data from the last element to the removed element's slot + for componentData in self.componentsData { + componentData.data + .swap(from: lastIndex, to: removedIndex) + componentData.changesTicks + .swap(from: lastIndex, to: removedIndex) + } + + // Update the entity that was in the last slot + let swappedEntityId = self.entities[lastIndex] + self.entities[removedIndex] = swappedEntityId + self.entityIndices[swappedEntityId] = removedIndex + self.entities.removeLast() + + return swappedEntityId + } else { + // The removed entity was the last one, so no swap is needed + self.entities.removeLast() + return nil + } + } + + func insert(at entityIndex: RowIndex, components: [any Component], tick: Tick) { + for component in components { + let componentId = type(of: component).identifier + guard let array = componentsData[componentId] else { + fatalError("Passed not registred component") + } + array.data.insert(component, at: entityIndex) + array.changesTicks.insert(tick, at: entityIndex) + } + } + + @inline(__always) + public func get(at entityIndex: RowIndex) -> T? { + return self.componentsData[T.identifier]?.data.get(at: entityIndex, as: T.self) + } + + public func isComponentChanged( + _ type: T.Type, + for entity: Entity.ID, + lastTick: Tick + ) -> Bool { + guard let entityIndex = self.entityIndices[entity] else { + return false + } + guard + let lastChangeTick = self.componentsData[T.identifier]? + .changesTicks + .get(at: entityIndex, as: Tick.self) + else { + return false + } + + return lastChangeTick >= lastTick + } + + @inline(__always) + public func get(_ type: T.Type, for entity: Entity.ID) -> T? { + guard let index = self.entityIndices[entity] else { + return nil + } + return self.componentsData[T.identifier]? + .data + .get(at: index, as: T.self) + } + + @inline(__always) + public func getMutablePointer( + _ type: T.Type, + for entity: Entity.ID + ) -> UnsafeMutablePointer? { + guard let index = self.entityIndices[entity] else { + return nil + } + return self.componentsData[T.identifier]? + .data + .getMutablePointer(at: index, as: T.self) + } + + public func getMutableTick( + _ type: T.Type, + for entity: Entity.ID + ) -> UnsafeMutablePointer? { + guard let index = self.entityIndices[entity] else { + return nil + } + return self.componentsData[T.identifier]? + .changesTicks + .getMutablePointer(at: index, as: Tick.self) + } + + public func insert( + _ component: consuming T, + at entityIndex: RowIndex, + lastTick: Tick + ) { + guard let componentData = self.componentsData[T.identifier] else { + assertionFailure("Component \(T.self) not found in chunk") + return + } + componentData.changesTicks.insert(lastTick, at: entityIndex) + componentData.data.insert(component, at: entityIndex) + } + + /// Get all component data arrays for efficient iteration + /// - Returns: Dictionary mapping component IDs to their data arrays + public func getAllComponentData() -> SparseSet { + return componentsData + } + + public func getComponents(for entity: Entity.ID) -> [(ComponentId, any Component)] { + guard let index = self.entityIndices[entity] else { + return [] + } + return componentsData.values.map { (key, data) in + (key, data.data.get(at: index, as: data.componentType)) + } + } + + public func getComponentSlice(for type: T.Type) -> UnsafeBufferPointer? { + guard let componentData = self.componentsData[T.identifier], self.count > 0 else { + return nil + } + let startPointer = componentData.data.getMutablePointer(at: 0, as: T.self) + return unsafe UnsafeBufferPointer(start: startPointer, count: self.count) + } + + public func getMutableComponentSlice(for type: T.Type) -> UnsafeMutablePointer? { + guard let componentData = self.componentsData[T.identifier], self.count > 0 else { + return nil + } + return componentData.data.getMutablePointer(at: 0, as: T.self) + } + + public func getComponentTicksSlice(for type: T.Type) -> UnsafeBufferPointer? { + guard let componentData = self.componentsData[T.identifier], self.count > 0 else { + return nil + } + let startPointer = componentData.changesTicks.getMutablePointer(at: 0, as: Tick.self) + return unsafe UnsafeBufferPointer(start: startPointer, count: self.count) + } + + public func getMutableComponentTicksSlice(for type: T.Type) -> UnsafeMutablePointer? { + guard let componentData = self.componentsData[T.identifier], self.count > 0 else { + return nil + } + return componentData.changesTicks.getMutablePointer(at: 0, as: Tick.self) + } +} + +extension Chunk: NonCopybaleCustomStringConvertible { + public var description: String { + return """ + Chunk( + entitiesPerChunk: \(entitiesPerChunk), + count: \(count), + entities: \(entities.map(\.description)), + entityIndices: \(entityIndices), + componentsData: + \(componentsData.map { $0.description }.joined(separator: "\n")) + ) + """ + } +} diff --git a/Sources/AdaECS/World/World+Storages.swift b/Sources/AdaECS/World/World+Storages.swift new file mode 100644 index 000000000..a8478daa0 --- /dev/null +++ b/Sources/AdaECS/World/World+Storages.swift @@ -0,0 +1,186 @@ +// +// World+Storages.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 20.11.2025. +// + +import AdaUtils + +extension World { + struct ComponentsStorage: Sendable { + struct RequiredComponentInfo: Sendable { + let id: ComponentId + let constructor: @Sendable () -> any Component + } + + private var components: [ComponentId] = [] + private var componentsIds: [ObjectIdentifier: ComponentId] = [:] + private var requiredComponents: [ComponentId: [RequiredComponentInfo]] = [:] + + mutating func registerRequiredComponent( + for component: ComponentId, + requiredComponentId: ComponentId, + constructor: @Sendable @escaping () -> T + ) { + var requiredComponents = self.requiredComponents[component] ?? [] + let newInfo = RequiredComponentInfo( + id: requiredComponentId, + constructor: constructor + ) + if let index = requiredComponents.firstIndex(where: { $0.id == requiredComponentId }) { + requiredComponents[index] = newInfo + } else { + requiredComponents.append(newInfo) + } + + self.requiredComponents[component] = requiredComponents + } + + @discardableResult + mutating func registerComponent() -> ComponentId { + let id = ComponentId(id: components.count) + self.components.append(id) + return id + } + + mutating func getOrRegisterComponent( + _ component: T.Type + ) -> ComponentId { + let id = ObjectIdentifier(T.self) + if let componentId = self.componentsIds[id] { + return componentId + } + let componentId = registerComponent() + componentsIds[id] = componentId + return componentId + } + + @inline(__always) + func getComponentId(_ component: T.Type) -> ComponentId? { + self.componentsIds[ObjectIdentifier(T.self)] + } + + func getRequiredComponents(for component: T) -> [RequiredComponentInfo] { + getComponentId(T.self).flatMap { self.requiredComponents[$0] } ?? [] + } + + func getRequiredComponents(for component: T.Type) -> [RequiredComponentInfo] { + getComponentId(T.self).flatMap { self.requiredComponents[$0] } ?? [] + } + } +} + +extension World { + struct Resources: Sendable { + @safe + struct ResourceData: @unchecked Sendable { + let pointer: BlobArray + let resourceType: any Resource.Type + let changedTick: UnsafeBox + + var erasedResource: any Resource { + pointer.get(at: 0, as: resourceType) + } + + func getWithTick( + _ type: T.Type + ) -> (pointer: UnsafeMutablePointer, changedTick: UnsafeBox) { + ( + pointer.getMutablePointer(at: 0, as: T.self), + changedTick + ) + } + } + + private var resources: [ComponentId] = [] + private var resourceIds: [ObjectIdentifier: ComponentId] = [:] + private var resourceData: SparseSet = [:] + + func getResource(_ resourceType: T.Type) -> T? { + guard let componentId = self.resourceIds[T.identifier], + let resource = self.resourceData[componentId] else { + return nil + } + return resource.pointer.get(at: 0, as: T.self) + } + + func getResources() -> Array { + self.resourceData.map { $0.erasedResource } + } + + mutating func getOrRegisterResource( + _ resource: any Resource.Type + ) -> ComponentId { + let id = resource.identifier + if let componentId = self.resourceIds[id] { + return componentId + } + return registerResource(resource, id: id) + } + + mutating func insertResource( + _ resource: consuming T, + tick: Tick + ) { + let componentId = self.getOrRegisterResource(T.self) + self.resourceData[componentId] = makeResourceData(resource, tick: tick) + } + + @discardableResult + mutating func registerComponent() -> ComponentId { + let id = ComponentId(id: resources.count) + self.resources.append(id) + return id + } + + mutating func registerResource( + _ resource: T.Type, + id: ObjectIdentifier + ) -> ComponentId { + Task { @MainActor in + T.registerResource() + } + let componentId = registerComponent() + self.resourceIds[id] = componentId + return componentId + } + + mutating func removeResource(_ resource: T.Type) { + let id = ObjectIdentifier(T.self) + guard let componentId = self.resourceIds[id] else { + return + } + self.resourceData[componentId] = nil + self.resourceIds[id] = nil + } + + func getResourceData(_ resource: T.Type) -> ResourceData? { + guard let componentId = self.resourceIds[T.identifier], + let resource = self.resourceData[componentId] else { + return nil + } + return resource + } + + mutating func clear() { + self.resources.removeAll() + self.resourceIds.removeAll() + self.resourceData.removeAll() + } + + private func makeResourceData(_ resource: consuming T, tick: Tick) -> ResourceData { + let array = unsafe BlobArray(count: 1, of: T.self) { pointer, count in + unsafe pointer.baseAddress? + .assumingMemoryBound(to: T.self) + .deinitialize(count: count) + } + array.insert(resource, at: 0) + return ResourceData( + pointer: array, + resourceType: T.self, + changedTick: UnsafeBox(tick) + ) + } + } +} diff --git a/Sources/AdaECS/World/World.swift b/Sources/AdaECS/World/World.swift index 969442bcd..e3ce1b5c9 100644 --- a/Sources/AdaECS/World/World.swift +++ b/Sources/AdaECS/World/World.swift @@ -7,10 +7,42 @@ import AdaUtils import Collections +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif +import Atomics -/// TODO: (Vlad) -/// [] Recalculate archetype for removed and added components. Archetype should use graph +public struct ChangeDetectionTick: Sendable { + public var change: UnsafeBox? + public let lastTick: Tick + public let currentTick: Tick + + public init(change: UnsafeBox?, lastTick: Tick, currentTick: Tick) { + self.change = change + self.lastTick = lastTick + self.currentTick = currentTick + } +} + +public struct Tick: Sendable, Comparable { + public let value: Int + + public init(value: Int) { + self.value = value + } + + public static func < (lhs: Tick, rhs: Tick) -> Bool { + lhs.value < rhs.value + } +} + +/// World syncronization actor. +@globalActor +public actor WorldActor { + public static let shared = WorldActor() +} /// Stores and exposes operations on ``Entity`` and ``Component``. /// @@ -26,27 +58,23 @@ public final class World: @unchecked Sendable, Codable { public let id = ID() public let name: String? - /// The records of the world. - private var records: OrderedDictionary = [:] + public private(set) var changeTick = ManagedAtomic(1) + public private(set) var lastTick: Tick = Tick(value: 0) + + /// The archetypes of the world. + public private(set) var entities: Entities = Entities() + public private(set) var archetypes: Archetypes = Archetypes() /// The removed entities of the world. internal private(set) var removedEntities: Set = [] - /// The added entities of the world. internal private(set) var addedEntities: Set = [] - - /// The archetypes of the world. - private(set) var archetypes: SparseArray = [] - - /// The free archetype indices of the world. - private var freeArchetypeIndices: [Int] = [] - /// The updated entities of the world. - private var updatedEntities: Set = [] - private var updatedComponents: [Entity: Set] = [:] + private var removedComponents: [Entity.ID: Set] = [:] private var componentsStorage = ComponentsStorage() - private var isReady = false + private var resources = Resources() + public var commandQueue: WorldCommandQueue = WorldCommandQueue() public private(set) var eventManager: EventManager = EventManager.default @@ -59,6 +87,13 @@ public final class World: @unchecked Sendable, Codable { self.name = name } + private init(from world: borrowing World) { + self.name = world.name + self.archetypes = world.archetypes + self.entities = world.entities + self.componentsStorage = world.componentsStorage + } + /// Initialize a new world from a decoder. /// - Parameter decoder: The decoder to initialize the world from. public init(from decoder: Decoder) throws { @@ -82,7 +117,7 @@ public final class World: @unchecked Sendable, Codable { if let decodable = resourceType as? Decodable.Type { let resource = try decodable.init(from: resourcesContainer.superDecoder(forKey: resourceKey)) - self.insertResource(resource as! Resource) + self.insertTypeErasedResource(resource as! Resource) } } @@ -92,90 +127,115 @@ public final class World: @unchecked Sendable, Codable { /// Encode the world to an encoder. /// - Parameter encoder: The encoder to encode the world to. public func encode(to encoder: Encoder) throws { + self.flush() + var container = encoder.container(keyedBy: CodingKeys.self) - let entities = (self.getEntities() + updatedEntities).sorted(by: { + let entities = self.getEntities().sorted(by: { $0.id < $1.id }) try container.encode(entities, forKey: .entities) var unkeyedContainer = container.nestedContainer(keyedBy: CodingName.self, forKey: .resources) - for resource in self.componentsStorage.resourceComponents.values { + for resource in self.resources.getResources() { try unkeyedContainer.encode(AnyEncodable(resource), forKey: CodingName(stringValue: type(of: resource).swiftName)) } } - // MARK: - Scheduler API + public func copy() -> World { + World(from: self) + } +} + +// MARK: - Scheduler API + +public extension World { /// Set the order of schedulers for this world. /// - Parameter schedulers: The schedulers to set. - public func setSchedulers(_ schedulers: [SchedulerName]) { + func setSchedulers(_ schedulers: [SchedulerName]) { self.schedulers.setSchedulers(schedulers) } /// Insert a scheduler before or after another scheduler. /// - Parameter scheduler: The scheduler to insert. /// - Parameter after: The scheduler after which to insert the new scheduler. - public func insertScheduler(_ scheduler: Scheduler, after: SchedulerName) { + func insertScheduler(_ scheduler: Scheduler, after: SchedulerName) { schedulers.insert(scheduler, after: after) } /// Insert a scheduler before or before another scheduler. /// - Parameter scheduler: The scheduler to insert. /// - Parameter before: The scheduler before which to insert the new scheduler. - public func insertScheduler(_ scheduler: Scheduler, before: SchedulerName) { + func insertScheduler(_ scheduler: Scheduler, before: SchedulerName) { schedulers.insert(scheduler, before: before) } /// Contains scheduler /// - Parameter scheduler: The scheduler to check. /// - Returns: True if the scheduler exists, otherwise false. - public func containsScheduler(_ scheduler: SchedulerName) -> Bool { + func containsScheduler(_ scheduler: SchedulerName) -> Bool { self.schedulers.contains(scheduler) } /// Add schedulers. /// - Parameter schedulers: The schedulers to add. - public func addSchedulers(_ schedulers: SchedulerName...) { + func addSchedulers(_ schedulers: SchedulerName...) { schedulers.forEach { self.schedulers.append(Scheduler(name: $0)) } } + /// Add scheduler + /// - Parameter scheduler: The scheduler to add. + func addScheduler(_ scheduler: Scheduler) { + self.schedulers.append(scheduler) + } + + /// Run a specific scheduler. + /// - Parameter scheduler: Scheduler name. + /// - Parameter deltaTime: Time interval since last update. + func runScheduler(_ schedulerName: SchedulerName) async { + await self.schedulers.getScope(for: schedulerName) { + await $0.run(world: self) + } + } +} + +// MARK: - Systems API + +public extension World { /// Add new system to the world. - /// - Warning: System should be added before build. /// - Parameter systemType: System type. /// Add a system to a specific scheduler. @discardableResult - public func addSystem(_ systemType: T.Type, on scheduler: SchedulerName) -> Self { - if self.isReady { - assertionFailure("Can't insert system if scene was ready") - return self - } - let system = systemType.init(world: self) - self.schedulers.getScheduler(scheduler)?.systemGraph.addSystem(system) + func addSystem(_ systemType: T.Type, on scheduler: SchedulerName) -> Self { + self.schedulers.addSystem( + systemType.init(world: self), + for: scheduler + ) return self } - /// Add new system to the world. - /// - Warning: System should be added before build. + /// Add new system to the world for `update` scheduler. /// - Parameter systemType: System type. /// - Returns: A world instance. @discardableResult - public func addSystem(_ systemType: T.Type) -> Self { + func addSystem(_ systemType: T.Type) -> Self { return addSystem(systemType, on: .update) } } +// MARK: - Entities managment + public extension World { /// Get all entities in world. /// - Complexity: O(n) /// - Returns: All entities in world. func getEntities() -> [Entity] { - return self.records.values.elements - .map { record in - let archetype = self.archetypes[record.archetypeId]! - return archetype.entities[record.row] + return self.entities.entities + .compactMap { location in + let archetype = self.archetypes.archetypes[location.archetypeId] + return archetype.entities[location.archetypeRow] } - .compactMap { $0 } } /// Get an entity by their id. @@ -183,12 +243,13 @@ public extension World { /// - Complexity: O(1) /// - Returns: Returns nil if entity not registed in scene world. func getEntityByID(_ entityID: Entity.ID) -> Entity? { - guard let record = self.records[entityID] else { + guard let location = self.entities.entities[entityID] else { return nil } - let archetype = self.archetypes[record.archetypeId] - return archetype?.entities[record.row] + return self.archetypes + .archetypes[location.archetypeId] + .entities[location.archetypeRow] } /// Find an entity by name. @@ -196,7 +257,7 @@ public extension World { /// - Complexity: O(n) /// - Returns: An entity with matched name or nil if entity with given name not exists. func getEntityByName(_ name: String) -> Entity? { - for arch in archetypes { + for arch in archetypes.archetypes { if let ent = arch.entities.first(where: { $0.name == name }) { return ent } @@ -205,40 +266,51 @@ public extension World { return nil } - /// Build the world. - /// - Note: This method should be called after all systems and resources are added. - func build() { - if isReady { - fatalError("World already configured") - } - isReady = true - for label in self.schedulers.schedulerLabels { - let scheduler = self.schedulers.getScheduler(label) - scheduler?.systemGraph.linkSystems() - } + /// Add a new entity to the world. + /// - Parameter entity: The entity to add. + /// - Returns: A world instance. + @discardableResult + func addEntity(_ entity: consuming Entity) -> Self { self.flush() + + if entity.id == Entity.notAllocatedId { + entities.addNotAllocatedEntity(entity) + } + + insertNewEntity( + entity, + components: Array(entity.components.notFlushedComponents) + ) + return self } - /// Add a new entity to the world. This entity will be available on the next update tick. - /// - Parameter entity: The entity to add. - /// - Parameter needsCopy: If true, the entity will be copied before adding to the world. + /// Remove entity from world. + /// - Parameter recursively: also remove entity child. /// - Returns: A world instance. @discardableResult - func addEntity(_ entity: Entity) -> Self { - entity.world = self + func removeEntity(_ entity: borrowing Entity, recursively: Bool = false) -> Self { + self.removeEntityRecord(entity.id) - self.updatedEntities.insert(entity) - self.addedEntities.insert(entity.id) + guard recursively && !entity.children.isEmpty else { + return self + } + + for child in entity.children { + self.removeEntity(child, recursively: recursively) + } - eventManager.send(WorldEvents.DidAddEntity(entity: entity), source: self) return self } - /// Remove entity from world. + /// Remove entity by id from world. /// - Parameter recursively: also remove entity child. /// - Returns: A world instance. @discardableResult - func removeEntity(_ entity: Entity, recursively: Bool = false) -> Self { + func removeEntity(_ entity: borrowing Entity.ID, recursively: Bool = false) -> Self { + guard let entity = self.getEntityByID(entity) else { + return self + } + self.removeEntityRecord(entity.id) guard recursively && !entity.children.isEmpty else { @@ -255,12 +327,13 @@ public extension World { /// Remove entity from world. /// - Note: Entity will removed on next `update` call. /// - Parameter recursively: also remove entity child. - func removeEntityOnNextTick(_ entity: Entity, recursively: Bool = false) { - guard self.records[entity.id] != nil else { + func removeEntityOnNextTick(_ entity: consuming Entity, recursively: Bool = false) { + guard self.entities.entities[entity.id] != nil else { return } - eventManager.send(WorldEvents.WillRemoveEntity(entity: entity), source: self) + let entity = entity + defer { eventManager.send(WorldEvents.WillRemoveEntity(entity: entity), source: self) } self.removedEntities.insert(entity.id) guard recursively && !entity.children.isEmpty else { @@ -271,14 +344,67 @@ public extension World { self.removeEntityOnNextTick(child, recursively: recursively) } } +} + +// MARK: - World utils + +public extension World { + func makeCommands() -> Commands { + Commands(entities: entities, commandsQueue: self.commandQueue.copy()) + } + + func flushCommands() { + guard !commandQueue.isEmpty else { + return + } + self.commandQueue.applyAndDrop(to: self) + } + /// Update all data in world. + /// In this step we move entities to matched archetypes and remove pending in delition entities. + func flush() { + self.flushCommands() + + for entityId in self.removedEntities { + self.removeEntityRecord(entityId) + } + } + + /// Clear trackers for entities, components and resources. + /// - Complexity: O(1) + func clearTrackers() { + self.removedEntities.removeAll(keepingCapacity: true) + self.addedEntities.removeAll(keepingCapacity: true) + self.lastTick = self.incrementChangeTick() + } + + /// Remove all data from world exclude resources. + /// - Complexity: O(n) + func clear() { + self.entities.clear() + self.archetypes.clear() + self.removedEntities.removeAll(keepingCapacity: true) + self.addedEntities.removeAll(keepingCapacity: true) + self.commandQueue = WorldCommandQueue() + } + + func incrementChangeTick() -> Tick { + let lastValue = self.changeTick.loadThenWrappingIncrement( + ordering: .relaxed + ) + return Tick(value: lastValue) + } +} + +// MARK: - Resource API + +public extension World { /// Insert a resource into the world. /// - Parameter resource: The resource to insert. /// - Returns: A world instance. @discardableResult func insertResource(_ resource: consuming T) -> Self { - let componentId = self.componentsStorage.getOrRegisterResource(T.self) - self.componentsStorage.resourceComponents[componentId] = resource + self.resources.insertResource(resource, tick: lastTick) return self } @@ -286,217 +412,277 @@ public extension World { /// - Parameter resource: The resource to insert. /// - Returns: A world instance. @discardableResult - func insertResource(_ resource: any Resource) -> Self { - let resource = resource - let componentId = self.componentsStorage.getOrRegisterResource(type(of: resource)) - self.componentsStorage.resourceComponents[componentId] = resource + private func insertTypeErasedResource(_ resource: consuming any Resource) -> Self { + self.resources.insertResource(resource, tick: lastTick) return self } + func createResource(of type: T.Type) -> T { + let resource = type.init(from: self) + self.resources.insertResource(resource, tick: lastTick) + return resource + } + /// Remove a resource from the world. /// - Parameter resource: The resource to remove. - func removeResource(_ resource: T.Type) { - self.componentsStorage.removeResource(resource) + consuming func removeResource(_ resource: T.Type) { + self.resources.removeResource(resource) } /// Get a resource from the world. /// - Parameter resource: The resource to get. - /// - Returns: The resource if it exists, otherwise nil. /// - Complexity: O(1) /// - Returns: The resource if it exists, otherwise nil. borrowing func getResource(_ resource: T.Type) -> T? { - return self.componentsStorage.getResource(resource) + return self.resources.getResource(resource) + } + + /// Get a resource from the world or initialize it if it doesn't exist. + /// - Parameter type: The type of the resource to get or initialize. + /// - Complexity: O(1) + /// - Returns: The resource if it exists, otherwise the initialized resource. + func getOrInitResource(of type: T.Type) -> T { + if let resource = self.resources.getResource(T.self) { + return resource + } + let resource = type.init(from: self) + resources.insertResource(resource, tick: lastTick) + return resource } + /// Get a resource from the world. + /// - Parameter resource: The resource to get. + /// - Complexity: O(1) + /// - Returns: The resource if it exists, otherwise nil. + func getRefResource(_ resource: T.Type) -> Ref { + let resource = self.resources.getResourceData(T.self)?.getWithTick(T.self) + return unsafe Ref( + pointer: resource?.pointer, + changeTick: .init( + change: resource?.changedTick, + lastTick: lastTick, + currentTick: lastTick + ) + ) + } + /// Get all resources from the world. /// - Returns: All resources in world. func getResources() -> [any Resource] { - return Array(self.componentsStorage.resourceComponents.values) + return self.resources.getResources() } - /// Check if component was changed for entity. - /// - Parameter component: Component identifier. - /// - Parameter entity: Entity. + /// Clear all resources from the world. /// - Complexity: O(1) - /// - Returns: True if component was changed for entity, otherwise false. - func isComponentChanged(_ component: T.Type, for entity: Entity) -> Bool { - return self.updatedComponents[entity]?.contains(T.identifier) ?? false + func clearResources() { + self.resources.clear() } +} - /// Run all schedulers in world. - /// - Parameter deltaTime: Time interval since last update. - @MainActor - func update(_ deltaTime: AdaUtils.TimeInterval) async { - self.flush() - self.clearTrackers() - - for label in self.schedulers.schedulerLabels { - guard let scheduler = self.schedulers.getScheduler(label) else { - continue - } +// MARK: - Entities and Components - await scheduler.graphExecutor.execute( - scheduler.systemGraph, - world: self, - deltaTime: deltaTime, - scheduler: scheduler.name - ) - } +public extension World { + @discardableResult + func spawn( + _ name: String = "", + @ComponentsBuilder components: () -> ComponentsBundle + ) -> Entity { + self.spawn(name, bundle: components()) } - /// Run a specific scheduler. - /// - Parameter scheduler: Scheduler name. - /// - Parameter deltaTime: Time interval since last update. - @MainActor - func runScheduler(_ scheduler: SchedulerName, deltaTime: AdaUtils.TimeInterval) async { - guard let scheduler = self.schedulers.getScheduler(scheduler) else { - fatalError("Scheduler \(scheduler) not found") - } - - await scheduler.graphExecutor.execute( - scheduler.systemGraph, - world: self, - deltaTime: deltaTime, - scheduler: scheduler.name - ) + @discardableResult + func spawn( + _ name: String = "", + bundle: consuming T + ) -> Entity { + let entity = entities.allocate(with: name) + insertNewEntity(entity, components: bundle.components) + return entity } - /// Update all data in world. - /// In this step we move entities to matched archetypes and remove pending in delition entities. - func flush() { - self.moveEntitiesToMatchedArchetypesIfNeeded() + @discardableResult + @inline(__always) + func spawn(_ name: String = "") -> Entity { + let entity = entities.allocate(with: name) + insertNewEntity(entity, components: []) + return entity + } - for entityId in self.removedEntities { - self.removeEntityRecord(entityId) + func get(from entity: Entity.ID) -> T? { + guard let location = self.entities.entities[entity] else { + return nil } + return self.archetypes + .archetypes[location.archetypeId] + .chunks + .chunks[location.chunkIndex] + .get(at: location.chunkRow) } - /// Clear trackers for entities, components and resources. - /// - Complexity: O(1) - func clearTrackers() { - self.removedEntities.removeAll(keepingCapacity: true) - self.addedEntities.removeAll(keepingCapacity: true) - self.updatedComponents.removeAll(keepingCapacity: true) + @inline(__always) + func get(_ type: T.Type, from entity: Entity.ID) -> T? { + return self.get(from: entity) } - /// Remove all data from world. - /// - Complexity: O(n) - func clear() { - self.records.removeAll(keepingCapacity: true) - self.updatedComponents.removeAll(keepingCapacity: true) - self.removedEntities.removeAll(keepingCapacity: true) - self.addedEntities.removeAll(keepingCapacity: true) - self.archetypes.removeAll(keepingCapacity: true) - self.freeArchetypeIndices.removeAll(keepingCapacity: true) - self.updatedEntities.removeAll(keepingCapacity: true) - } -} + func insert(_ component: consuming T, for entityId: Entity.ID) { + guard let location = self.entities.entities[entityId] else { + return + } -// MARK: - Delegate + // We have component in archetype, just update + var archetype = self.archetypes.archetypes[location.archetypeId] + if archetype.componentLayout.maskSet.contains(T.identifier) { + self.archetypes + .archetypes[location.archetypeId] + .chunks + .chunks[location.chunkIndex] + .insert(component, at: location.chunkRow, lastTick: lastTick) + return + } -extension World { - /// Entity did add component. - /// - Parameter entity: The entity that did add component. - /// - Parameter component: The component that did add. - /// - Parameter identifier: The identifier of the component. - func entity(_ entity: Entity, didAddComponent component: T.Type, with identifier: ComponentId) { - eventManager.send(ComponentEvents.DidAdd(componentType: component, entity: entity)) - self.updatedEntities.insert(entity) - } - - /// Entity did update component. - /// - Parameter entity: The entity that did update component. - /// - Parameter component: The component that did update. - /// - Parameter identifier: The identifier of the component. - func entity(_ entity: Entity, didUpdateComponent component: T.Type, with identifier: ComponentId) { - eventManager.send(ComponentEvents.DidChange(componentType: component, entity: entity)) - - self.updatedEntities.insert(entity) - self.updatedComponents[entity, default: []].insert(identifier) - } - - /// Entity did remove component. - /// - Parameter entity: The entity that did remove component. - /// - Parameter component: The component that did remove. - /// - Parameter identifier: The identifier of the component. - func entity(_ entity: Entity, didRemoveComponent component: Component.Type, with identifier: ComponentId) { - eventManager.send(ComponentEvents.WillRemove(componentType: component, entity: entity)) - self.updatedEntities.insert(entity) - } -} + // Prepare new layout + var newLayout = archetype.componentLayout + newLayout.insert(T.self) -private extension World { - /// Remove entity record. - /// - Parameter entity: The entity to remove. - private func removeEntityRecord(_ entity: Entity.ID) { - guard let record = self.records[entity] else { - return + var components: [any Component] = [] + for requiredComponent in componentsStorage.getRequiredComponents(for: T.self) { + let newComponent = requiredComponent.constructor() + components.append(newComponent) + newLayout.insert(type(of: newComponent)) } - self.records[entity] = nil - guard var currentArchetype = self.archetypes[record.archetypeId] else { - assertionFailure("Incorrect record of archetype \(record)") + // Move entity to new archetype + if let newArchetype = archetype.edges.getArchetypeAfterInsertion(for: newLayout) { + self.moveEntityToArchetype( + entityId, + oldLocation: location, + newArchetype: newArchetype + ) + } else { + let newArchetype = self.archetypes.getOrCreate(for: newLayout) + archetype.edges.addArchetypeAfterInsertion(newArchetype, for: newLayout) + self.archetypes.archetypes[location.archetypeId] = archetype + self.moveEntityToArchetype( + entityId, + oldLocation: location, + newArchetype: newArchetype + ) + } + + // Insert components + guard let newLocation = self.entities.entities[entityId] else { + assertionFailure("Failed to insert component to entity") return } - currentArchetype.remove(at: record.row) - if currentArchetype.entities.isEmpty { - self.archetypes[record.archetypeId]!.clear() - self.freeArchetypeIndices.append(record.archetypeId) + for component in components { + self.archetypes + .archetypes[newLocation.archetypeId] + .chunks + .insert(component, for: entityId, lastTick: lastTick) } - self.archetypes[record.archetypeId] = currentArchetype + self.archetypes + .archetypes[newLocation.archetypeId] + .chunks + .insert(component, for: entityId, lastTick: lastTick) + } + + @inline(__always) + func remove(_ component: consuming T, for entity: Entity.ID) { + self.remove(T.identifier, from: entity) } - /// Find or create matched arhcetypes for all entities that wait update - private func moveEntitiesToMatchedArchetypesIfNeeded() { - guard !self.updatedEntities.isEmpty else { + /// Remove a component of the specified type from an entity. + /// - Parameter componentType: The type of component to remove. + /// - Parameter entity: The entity ID to remove the component from. + @inline(__always) + func remove(_ componentType: T.Type, from entityId: Entity.ID) { + guard let location = entities.entities[entityId] else { return } + let entity = self.archetypes.archetypes[location.archetypeId].entities[location.archetypeRow] + eventManager.send(ComponentEvents.WillRemove(componentType: T.self, entity: entity)) + self.remove(T.identifier, from: entityId) + } - for entity in self.updatedEntities { - let bitmask = entity.components.bitset + func remove(_ componentId: ComponentId, from entityId: Entity.ID) { + // Get the entity's current location + guard let location = self.entities.entities[entityId] else { + return // Entity doesn't exist in the world + } - if let record = self.records[entity.id], var currentArchetype = self.archetypes[record.archetypeId] { - // We currently updated existed components - if currentArchetype.componentsBitMask == bitmask { - continue - } + // Get the entity from the archetype + var archetype = self.archetypes.archetypes[location.archetypeId] + var newLayout = archetype.chunks.componentLayout + newLayout.remove(componentId) + if let newArchetype = archetype.edges.getArchetypeAfterRemoval(for: newLayout) { + self.moveEntityToArchetype( + entityId, + oldLocation: location, + newArchetype: newArchetype + ) + } else { + let newArchetype = self.archetypes.getOrCreate(for: newLayout) + archetype.edges.addArchetypeAfterRemoval(newArchetype, for: newLayout) + self.archetypes.archetypes[location.archetypeId] = archetype + self.moveEntityToArchetype( + entityId, + oldLocation: location, + newArchetype: newArchetype + ) + } + self.removedComponents[entityId, default: []].insert(componentId) + } - currentArchetype.remove(at: record.row) - self.archetypes[record.archetypeId] = currentArchetype - } + @inline(__always) + @discardableResult + func registerRequiredComponent( + _ requiredComponent: R.Type, + for component: T.Type + ) -> Self { + self.registerRequiredComponent(requiredComponent, for: component) { + R.defaultValue + } + } - // Previous archetype doesn't match for an entity bit mask, try to find a new one - var archetype = self.archetypes.first(where: { - $0.componentsBitMask == bitmask - }) - - // We don't have matched archetype -> create a new one - if archetype == nil { - var newArch: Archetype - - if self.freeArchetypeIndices.isEmpty { - newArch = Archetype.new(index: self.archetypes.count) - - self.archetypes.append(newArch) - } else { - let index = self.freeArchetypeIndices.removeFirst() - newArch = self.archetypes[index]! - } - newArch.componentsBitMask = bitmask - archetype = newArch - } + @discardableResult + func registerRequiredComponent( + _ requiredComponent: R.Type, + for component: T.Type, + constructor: @Sendable @escaping () -> R + ) -> Self { + let componentId = self.componentsStorage.getOrRegisterComponent(component) + let requiredComponentId = self.componentsStorage.getOrRegisterComponent(requiredComponent) + self.componentsStorage.registerRequiredComponent( + for: componentId, + requiredComponentId: requiredComponentId, + constructor: constructor + ) + + return self + } - let location = archetype?.append(entity) - self.records[entity.id] = location - self.archetypes[archetype!.id] = archetype + @inline(__always) + func has(_ type: T.Type, in entity: Entity.ID) -> Bool { + self.has(T.identifier, in: entity) + } + + func has(_ identifier: ComponentId, in entity: Entity.ID) -> Bool { + guard let location = self.entities.entities[entity] else { + return false } - self.updatedEntities.removeAll(keepingCapacity: true) + return self.archetypes + .archetypes[location.archetypeId] + .componentLayout + .maskSet + .contains(identifier) } } +// MARK: - Queries + extension World { /// Returns all entities of the scene which pass the ``QueryPredicate`` of the query. public func performQuery(_ query: EntityQuery) -> EntityQuery.Result { @@ -504,6 +690,15 @@ extension World { state.updateArchetypes(in: self) return QueryResult(state: state) } + + /// Returns all components of the scene which pass the ``FilterQuery``. + public func performQuery( + _ query: FilterQuery + ) -> QueryResult.Builder, F> { + let state = query.state + state.updateArchetypes(in: self) + return QueryResult(state: state) + } } extension World: EventSource { @@ -521,6 +716,109 @@ extension World: EventSource { } } +extension World { + /// Insert entity to the world. Expect, that entity is already stored in `Entities`. + func insertNewEntity(_ entity: Entity, components: [any Component]) { + let components: [any Component] = components.reduce(into: []) { partialResult, component in + for requiredComponent in componentsStorage.getRequiredComponents(for: component) { + partialResult.append(requiredComponent.constructor()) + } + partialResult.append(component) + } + let componentsLayout = ComponentLayout(components: components) + let archetypeIndex = self.archetypes.getOrCreate( + for: componentsLayout + ) + + var archetype = self.archetypes.archetypes[archetypeIndex] + let row = archetype.append(entity) + let chunkLocation = archetype.chunks.insertEntity( + entity.id, + components: components, + tick: self.lastTick + ) + self.archetypes.archetypes[archetypeIndex] = archetype + self.entities.entities[entity.id] = EntityLocation( + archetypeId: archetype.id, + archetypeRow: row, + chunkIndex: chunkLocation.chunkIndex, + chunkRow: chunkLocation.entityRow + ) + entity.world = self + addedEntities.insert(entity.id) + eventManager.send(WorldEvents.DidAddEntity(entity: entity), source: self) + } + + /// Move entity to new archetype. + private func moveEntityToArchetype( + _ entityId: Entity.ID, + oldLocation location: EntityLocation, + newArchetype: Archetype.ID + ) { + var archetype = self.archetypes.archetypes[location.archetypeId] + let entity = archetype.entities[location.archetypeRow] + var toArchetype = self.archetypes.archetypes[newArchetype] + let row = toArchetype.append(entity) + let result = archetype.swapRemove(at: location.archetypeRow) + let newLocation = archetype.chunks.moveEntity(entityId, to: &toArchetype.chunks).newLocation + if let swappedEntity = result.swappedEntity { + entities.entities[swappedEntity] = EntityLocation( + archetypeId: location.archetypeId, + archetypeRow: location.archetypeRow, + chunkIndex: location.chunkIndex, + chunkRow: location.chunkRow + ) + } + + self.archetypes.archetypes[location.archetypeId] = archetype + self.archetypes.archetypes[newArchetype] = toArchetype + self.entities.entities[entityId] = EntityLocation( + archetypeId: newArchetype, + archetypeRow: row, + chunkIndex: newLocation.chunkIndex, + chunkRow: newLocation.entityRow + ) + } + + /// Remove entity record. + /// - Parameter entity: The entity to remove. + private func removeEntityRecord(_ entity: Entity.ID) { + guard let record = self.entities.entities[entity] else { + return + } + self.entities.entities[entity] = nil + + var currentArchetype = self.archetypes.archetypes[record.archetypeId] + let removeResult = currentArchetype.swapRemove(at: record.archetypeRow) + + if + let swappedEntity = removeResult.swappedEntity, + let swappedLocation = entities.entities[swappedEntity] + { + entities.entities[swappedEntity] = EntityLocation( + archetypeId: swappedLocation.archetypeId, + archetypeRow: record.archetypeRow, + chunkIndex: swappedLocation.chunkIndex, + chunkRow: swappedLocation.chunkRow + ) + } + + let removeChunkResult = currentArchetype.chunks.removeEntity(entity) + if let removeChunkResult, let swappedEntity = removeChunkResult.swappedEntity { + if let swappedLocation = entities.entities[swappedEntity] { + entities.entities[swappedEntity] = EntityLocation( + archetypeId: swappedLocation.archetypeId, + archetypeRow: swappedLocation.archetypeRow, + chunkIndex: removeChunkResult.newLocation.chunkIndex, + chunkRow: removeChunkResult.newLocation.entityRow + ) + } + } + + self.archetypes.archetypes[record.archetypeId] = currentArchetype + } +} + /// Events the world triggers. public enum WorldEvents { /// Raised after an entity is added to the scene. @@ -543,62 +841,3 @@ private extension World { case plugins } } - -extension World { - struct ComponentsStorage: Sendable { - var components: [ComponentId] = [] - var resourceIds: [ObjectIdentifier: ComponentId] = [:] - var resourceComponents: [ComponentId: any Resource] = [:] - - @discardableResult - mutating func registerComponent() -> ComponentId { - let id = ComponentId(id: components.count) - self.components.append(id) - return id - } - - mutating func getOrRegisterComponent( - _ component: T.Type - ) -> ComponentId { - let id = ObjectIdentifier(T.self) - if let componentId = self.resourceIds[id] { - return componentId - } - return registerComponent() - } - - mutating func getOrRegisterResource( - _ resource: any Resource.Type - ) -> ComponentId { - let id = resource.identifier - if let componentId = self.resourceIds[id] { - return componentId - } - return registerResource(resource) - } - - mutating func registerResource(_ resource: T.Type) -> ComponentId { - let id = ObjectIdentifier(T.self) - let componentId = registerComponent() - self.resourceIds[id] = componentId - return componentId - } - - func getResource(_ resource: T.Type) -> T? { - let id = ObjectIdentifier(T.self) - guard let componentId = self.resourceIds[id] else { - return nil - } - return self.resourceComponents[componentId] as? T - } - - mutating func removeResource(_ resource: T.Type) { - let id = ObjectIdentifier(T.self) - guard let componentId = self.resourceIds[id] else { - return - } - self.resourceComponents[componentId] = nil - self.resourceIds[id] = nil - } - } -} diff --git a/Sources/AdaEditor/AdaEditorApp.swift b/Sources/AdaEditor/AdaEditorApp.swift index 8a7f8d40d..99cbeac51 100644 --- a/Sources/AdaEditor/AdaEditorApp.swift +++ b/Sources/AdaEditor/AdaEditorApp.swift @@ -11,22 +11,23 @@ import Logging @main struct AdaEditorApp: App { var body: some AppScene { - EmptyWindow() - .addPlugins( - DefaultPlugins(), - GameScene2DPlugin() - ) - .windowMode(.windowed) - .windowTitle("AdaEngine") + WindowGroup { + Text("See you later") + } + .addPlugins( + DefaultPlugins() + ) + .windowMode(.windowed) + .windowTitle("AdaEngine") } } -public extension Bundle { - static var editor: Bundle { +public extension Foundation.Bundle { + static var editor: Foundation.Bundle { #if SWIFT_PACKAGE && !BAZEL_BUILD - return Bundle.module + return Foundation.Bundle.module #else - return Bundle(for: BundleToken.self) + return Foundation.Bundle(for: BundleToken.self) #endif } } diff --git a/Sources/AdaEditor/Assets/WindlessSlopes.ogg b/Sources/AdaEditor/Assets/WindlessSlopes.ogg deleted file mode 100644 index 87cd9f08a..000000000 --- a/Sources/AdaEditor/Assets/WindlessSlopes.ogg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:b0139f731c174fdee2259aaab1d943742b2095ddc81c7c0941b1edb74c92245a -size 1518674 diff --git a/Sources/AdaEditor/Assets/WindlessSlopes.wav b/Sources/AdaEditor/Assets/WindlessSlopes.wav deleted file mode 100644 index d6bb581de..000000000 --- a/Sources/AdaEditor/Assets/WindlessSlopes.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:0eb2c6551c1cd38991eb5968bd1d0249fd9ceb1bf1d628a1f78eec1ea6c28729 -size 18599468 diff --git a/Sources/AdaEditor/Assets/characters_packed.png b/Sources/AdaEditor/Assets/characters_packed.png deleted file mode 100644 index 9dc045db7..000000000 --- a/Sources/AdaEditor/Assets/characters_packed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:1702606cb8510ac8454dd93204f3f460d06a0d5071b080c86e856bb684abb929 -size 1973 diff --git a/Sources/AdaEditor/Assets/dog.png b/Sources/AdaEditor/Assets/dog.png deleted file mode 100644 index 2ff96169c..000000000 --- a/Sources/AdaEditor/Assets/dog.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:08d88d78904f8e1e4335f4ff09920854e8b203410acb6e960086a85463158cf7 -size 332646 diff --git a/Sources/AdaEditor/Assets/explosion-1.wav b/Sources/AdaEditor/Assets/explosion-1.wav deleted file mode 100644 index aa66b1c96..000000000 --- a/Sources/AdaEditor/Assets/explosion-1.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:610885a4c9219925fefb27e555d413dd7476addc65ab0ddd3b70a189b6b72909 -size 36028 diff --git a/Sources/AdaEditor/Assets/explosion-2.wav b/Sources/AdaEditor/Assets/explosion-2.wav deleted file mode 100644 index 7597adc40..000000000 --- a/Sources/AdaEditor/Assets/explosion-2.wav +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:493d98937003c30e707358d97fbb9ab39405268dd9152e7c471b9eb7a60d0662 -size 20892 diff --git a/Sources/AdaEditor/Assets/explosion.png b/Sources/AdaEditor/Assets/explosion.png deleted file mode 100644 index 16c4b4107..000000000 --- a/Sources/AdaEditor/Assets/explosion.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:ba1b7e458dc36c19548d53445129adc89570ab64fa1ada370db9a33bd8449699 -size 1935 diff --git a/Sources/AdaEditor/Assets/tiles.png b/Sources/AdaEditor/Assets/tiles.png deleted file mode 100644 index e9467dc7d..000000000 --- a/Sources/AdaEditor/Assets/tiles.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:b2860c7ee9d046abcc00c2e6c8c98099a54bca2bb44c5299ee41e8fa0a0e9347 -size 6185 diff --git a/Sources/AdaEditor/Assets/tiles_packed.png b/Sources/AdaEditor/Assets/tiles_packed.png deleted file mode 100644 index 350484575..000000000 --- a/Sources/AdaEditor/Assets/tiles_packed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git.lfs.github.com/spec/v1 -oid sha256:0d58b43a0e6a6818a76c6dbba176299cb10538aab2ee530bce9e856702d956fa -size 5811 diff --git a/Sources/AdaEditor/Components/EditorCamera.swift b/Sources/AdaEditor/Components/EditorCamera.swift index f89d6a6bc..25a70eac2 100644 --- a/Sources/AdaEditor/Components/EditorCamera.swift +++ b/Sources/AdaEditor/Components/EditorCamera.swift @@ -1,112 +1,112 @@ +//// +//// EditorCamera.swift +//// AdaEngine +//// +//// Created by v.prusakov on 5/21/22. +//// // -// EditorCamera.swift -// AdaEngine -// -// Created by v.prusakov on 5/21/22. -// - -import AdaEngine - -@Component -struct EditorCameraComponent { - var speed: Float = 20 - var pitch: Angle = Angle.radians(0) - var yaw: Angle = Angle.radians(-90) -} - -class EditorCameraSystem: System { - - static let query = EntityQuery(where: .has(EditorCameraComponent.self) && .has(Camera.self)) - - private var cameraUp: Vector3 = Vector3(0, 1, 0) - private var cameraFront: Vector3 = Vector3(0, 0, -1) - - private var lastMousePosition: Point = .zero - - private var isViewMatrixDirty = false - - required public init(world: World) { } - - public func update(context: inout UpdateContext) { - let entities = context.world.performQuery(Self.query) - let deltaTime = context.deltaTime - - for entity in entities { - - var (editorCamera, camera, transform) = entity.components[EditorCameraComponent.self, Camera.self, Transform.self] - let speed = editorCamera.speed - - if Input.isKeyPressed(.w) { - transform.position += speed * cameraFront * deltaTime - self.isViewMatrixDirty = true - } - - if Input.isKeyPressed(.a) { - transform.position -= cross(cameraFront, cameraUp).normalized * speed * deltaTime - self.isViewMatrixDirty = true - } - - if Input.isKeyPressed(.d) { - transform.position += cross(cameraFront, cameraUp).normalized * speed * deltaTime - self.isViewMatrixDirty = true - } - - if Input.isKeyPressed(.s) { - transform.position -= speed * cameraFront * deltaTime - self.isViewMatrixDirty = true - } - - if self.isViewMatrixDirty { - camera.viewMatrix = Transform3D.lookAt( - eye: transform.position, - center: transform.position + self.cameraFront, - up: self.cameraUp - ) - - self.isViewMatrixDirty = false - } - - // Apply transform - entity.components[EditorCameraComponent.self] = editorCamera - entity.components[Camera.self] = camera - entity.components[Transform.self] = transform - } - } - - func mouseEvent(for editorComponent: inout EditorCameraComponent) { -// let position = Input.getMousePosition() -// var xoffset = position.x - self.lastMousePosition.x -// var yoffset = self.lastMousePosition.y - position.y -// self.lastMousePosition = position -// -// let sensitivity: Float = 0.1 -// xoffset *= sensitivity -// yoffset *= sensitivity -// -// editorComponent.yaw += xoffset -// editorComponent.pitch += yoffset -// -// if editorComponent.pitch.radians > 89.0 { -// editorComponent.pitch = 89.0 -// } else if(editorComponent.pitch.radians < -89.0) { -// editorComponent.pitch = -89.0 -// } -// -// var direction = Vector3() -// direction.x = Math.cos(editorComponent.yaw.radians) * Math.cos(editorComponent.pitch.radians) -// direction.y = Math.sin(editorComponent.pitch.radians) -// direction.z = Math.sin(editorComponent.yaw.radians) * Math.cos(editorComponent.pitch.radians) -// -// self.cameraFront = direction.normalized +//import AdaEngine +// +//@Component +//struct EditorCameraComponent { +// var speed: Float = 20 +// var pitch: Angle = Angle.radians(0) +// var yaw: Angle = Angle.radians(-90) +//} +// +//class EditorCameraSystem: System { +// +// static let query = EntityQuery(where: .has(EditorCameraComponent.self) && .has(Camera.self)) +// +// private var cameraUp: Vector3 = Vector3(0, 1, 0) +// private var cameraFront: Vector3 = Vector3(0, 0, -1) +// +// private var lastMousePosition: Point = .zero +// +// private var isViewMatrixDirty = false +// +// required public init(world: World) { } +// +// public func update(context: UpdateContext) { +// let entities = context.world.performQuery(Self.query) +// let deltaTime = context.deltaTime +// +// for entity in entities { +// +// var (editorCamera, camera, transform) = entity.components[EditorCameraComponent.self, Camera.self, Transform.self] +// let speed = editorCamera.speed +// +// if Input.isKeyPressed(.w) { +// transform.position += speed * cameraFront * deltaTime // self.isViewMatrixDirty = true - } -} - -class EditorCameraEntity: Entity, @unchecked Sendable { - public override init(name: String = "Entity") { - super.init(name: name) - - self.components += EditorCameraComponent() - self.components += Camera() - } -} +// } +// +// if Input.isKeyPressed(.a) { +// transform.position -= cross(cameraFront, cameraUp).normalized * speed * deltaTime +// self.isViewMatrixDirty = true +// } +// +// if Input.isKeyPressed(.d) { +// transform.position += cross(cameraFront, cameraUp).normalized * speed * deltaTime +// self.isViewMatrixDirty = true +// } +// +// if Input.isKeyPressed(.s) { +// transform.position -= speed * cameraFront * deltaTime +// self.isViewMatrixDirty = true +// } +// +// if self.isViewMatrixDirty { +// camera.viewMatrix = Transform3D.lookAt( +// eye: transform.position, +// center: transform.position + self.cameraFront, +// up: self.cameraUp +// ) +// +// self.isViewMatrixDirty = false +// } +// +// // Apply transform +// entity.components[EditorCameraComponent.self] = editorCamera +// entity.components[Camera.self] = camera +// entity.components[Transform.self] = transform +// } +// } +// +// func mouseEvent(for editorComponent: inout EditorCameraComponent) { +//// let position = Input.getMousePosition() +//// var xoffset = position.x - self.lastMousePosition.x +//// var yoffset = self.lastMousePosition.y - position.y +//// self.lastMousePosition = position +//// +//// let sensitivity: Float = 0.1 +//// xoffset *= sensitivity +//// yoffset *= sensitivity +//// +//// editorComponent.yaw += xoffset +//// editorComponent.pitch += yoffset +//// +//// if editorComponent.pitch.radians > 89.0 { +//// editorComponent.pitch = 89.0 +//// } else if(editorComponent.pitch.radians < -89.0) { +//// editorComponent.pitch = -89.0 +//// } +//// +//// var direction = Vector3() +//// direction.x = Math.cos(editorComponent.yaw.radians) * Math.cos(editorComponent.pitch.radians) +//// direction.y = Math.sin(editorComponent.pitch.radians) +//// direction.z = Math.sin(editorComponent.yaw.radians) * Math.cos(editorComponent.pitch.radians) +//// +//// self.cameraFront = direction.normalized +//// self.isViewMatrixDirty = true +// } +//} +// +//class EditorCameraEntity: Entity, @unchecked Sendable { +// public override init(name: String = "Entity") { +// super.init(name: name) +// +// self.components += EditorCameraComponent() +// self.components += Camera() +// } +//} diff --git a/Sources/AdaEditor/Scenes/BunnyExampleScene.swift b/Sources/AdaEditor/Scenes/BunnyExampleScene.swift deleted file mode 100644 index 4fcade2aa..000000000 --- a/Sources/AdaEditor/Scenes/BunnyExampleScene.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// StressExampleScene.swift -// -// -// Created by Vladislav Prusakov on 06.06.2024. -// - -import AdaEngine - -final class ManySpritesExampleScene: Scene, @unchecked Sendable { - override func sceneDidMove(to view: SceneView) { - let tilesImage = try! AssetsManager.loadSync( - Image.self, - at: "Assets/tiles_packed.png", - from: Bundle.editor - ).asset - - let characterAtlas = TextureAtlas(from: tilesImage, size: [18, 18]) - - self.spawnEntityes(atlas: characterAtlas) - - let cameraEntity = OrthographicCamera() - cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) - cameraEntity.camera.clearFlags = .solid - cameraEntity.camera.orthographicScale = 20 - - world.addEntity(cameraEntity) - world.addSystem(CamMovementSystem.self) - } - - func spawnEntityes(atlas: TextureAtlas) { - let mapSize = Vector2(100) - - let halfX = Int(mapSize.x / 2.0) - let halfY = Int(mapSize.y / 2.0) - - var entities: Int = 0 - - for y in -halfY.. = [] - - func setup(in app: AppWorlds) { - do { - let tiles = try AssetsManager.loadSync( - Image.self, - at: "@res://tiles_packed.png" - ).asset - let charactersTiles = try AssetsManager.loadSync( - Image.self, - at: "@res://characters_packed.png" - ).asset - - self.textureAtlas = TextureAtlas(from: tiles, size: [18, 18]) - self.characterAtlas = TextureAtlas(from: charactersTiles, size: [20, 23], margin: [4, 1]) - } catch { - fatalError(error.localizedDescription) - } - - let cameraEntity = OrthographicCamera() - cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) - cameraEntity.camera.clearFlags = .solid - cameraEntity.camera.orthographicScale = 1.1 - app.mainWorld.addEntity(cameraEntity) - - self.makePlayer(app.mainWorld) -// self.makeSubsceneAndSave(app) - self.loadSubscene(app) -// // try! self.makeCanvasItem(position: [-0.3, 0.4, -1]) - self.collisionHandler(app) -// - app - .addSystem(PlayerMovementSystem.self) - .addSystem(SpawnPhysicsBodiesSystem.self) - } - - private func collisionHandler(_ app: AppWorlds) { - app.mainWorld.subscribe(to: CollisionEvents.Began.self) { event in - if event.entityA.name == "Player" && (event.entityB.name == "Tube") { - // event.entityA.scene?.removeEntity(event.entityA) - // print("collide with tube") - // self.gameOver() - } - } - .store(in: &disposeBag) - } - - private func makePlayer(_ world: World) { - var transform = Transform() - transform.scale = [0.2, 0.2, 0.2] - - let playerTexture = AnimatedTexture() - playerTexture.framesPerSecond = 5 - playerTexture.framesCount = 2 - playerTexture[0] = self.characterAtlas[0, 0] - playerTexture[1] = self.characterAtlas[1, 0] - - let playerEntity = Entity(name: "Player") - playerEntity.components += SpriteComponent(texture: playerTexture) - playerEntity.components += transform - playerEntity.components += PhysicsBody2DComponent( - shapes: [ - .generateBox() - ], - mass: 1, - mode: .kinematic - ) - playerEntity.components += PlayerComponent() - world.addEntity(playerEntity) - } - - private func makeSubsceneAndSave(_ app: AppWorlds) { - let scene = Scene() - - var transform = Transform() - transform.scale = [3, 0.19, 0.19] - transform.position.y = -1 - - let untexturedEntity = Entity(name: "Ground") - untexturedEntity.components += SpriteComponent(texture: self.textureAtlas[0, 0]) - untexturedEntity.components += transform - untexturedEntity.components += Collision2DComponent( - shapes: [ - .generateBox() - ] - ) - - transform.position.y = -1.5 - - let texturedEntity = Entity(name: "Ground 2") - texturedEntity.components += SpriteComponent(tintColor: .red) - texturedEntity.components += Transform() - .setPosition(Vector3(0, 0.3, 0)) - .setScale(Vector3(0.49, 0.49, 0.49)) - texturedEntity.components += Collision2DComponent( - shapes: [ - .generateBox() - ] - ) - - scene.world.addEntity(untexturedEntity) - scene.world.addEntity(texturedEntity) - - Task { - try await AssetsManager.save(scene, at: "@res://", name: "Subscene.ascn") - - await MainActor.run { - self.loadSubscene(app) - } - } - } - - private func loadSubscene(_ app: AppWorlds) { - Task { @MainActor in - do { - let scene = try await AssetsManager.load( - Scene.self, - at: "@res://Subscene.ascn", - handleChanges: true - ) - app.mainWorld.addEntity( - Entity(name: "Subscene") { - DynamicScene(scene: scene) - } - ) - } catch { - print(error) - } - } - } -} - -@System(dependencies: [ - .before(CameraSystem.self) -]) -struct PlayerMovementSystem { - - static let playerQuery = EntityQuery(where: .has(PlayerComponent.self) && .has(PhysicsBody2DComponent.self)) - - @Query, Ref, GlobalTransform> - private var cameraQuery - static let matQuery = EntityQuery(where: .has(Mesh2DComponent.self) && .has(Transform.self)) - - init(world: World) { } - - // swiftlint:disable:next function_body_length cyclomatic_complexity - func update(context: inout UpdateContext) { - for (camera, cameraTransform, globalTransform) in cameraQuery { - let speed: Float = 2 * context.deltaTime - - // --- Gamepad camera movement --- - if let gamepad = Input.getConnectedGamepads().first { - let leftStickX = gamepad.getAxisValue(.leftStickX) - let leftStickY = gamepad.getAxisValue(.leftStickY) - let deadzone: Float = 0.1 - if abs(leftStickX) > deadzone { - cameraTransform.position.x += leftStickX * speed - } - if abs(leftStickY) > deadzone { - cameraTransform.position.y += leftStickY * speed // Invert Y for typical 2D controls - } - - let rightStickY = gamepad.getAxisValue(.rightStickY) - if abs(rightStickY) > deadzone { - camera.orthographicScale -= rightStickY * speed // Invert Y for typical 2D controls - } - } - // --- End gamepad camera movement --- - - if Input.isKeyPressed(.w) { - cameraTransform.position.y += speed - } - - if Input.isKeyPressed(.s) { - cameraTransform.position.y -= speed - } - - if Input.isKeyPressed(.a) { - cameraTransform.position.x -= speed - } - - if Input.isKeyPressed(.d) { - cameraTransform.position.x += speed - } - - if Input.isKeyPressed(.arrowUp) { - camera.orthographicScale -= speed - } - - if Input.isKeyPressed(.arrowDown) { - camera.orthographicScale += speed - } - - context.world.performQuery(Self.matQuery).forEach { entity in - let meshComponent = entity.components[Mesh2DComponent.self]! - if Input.isMouseButtonPressed(.left) { - (meshComponent.materials[0] as? CustomMaterial)?.color = .mint - } else { - (meshComponent.materials[0] as? CustomMaterial)?.color = .pink - } - - (meshComponent.materials[0] as? CustomMaterial)?.time += context.deltaTime - - var transform = entity.components[Transform.self]! - - if Input.isMouseButtonPressed(.left) { - let mousePosition = Input.getMousePosition() - if let position = camera.wrappedValue.viewportToWorld2D(cameraGlobalTransform: globalTransform.matrix, viewportPosition: mousePosition) { - // let values = context.scene.physicsWorld2D?.raycast(from: .zero, to: position) - - transform.position.x = position.x - transform.position.y = -position.y - } - } - - let speed: Float = 3 - - if Input.isKeyPressed(.semicolon) { - transform.position.x += speed * context.deltaTime - } - - if Input.isKeyPressed(.k) { - transform.position.x -= speed * context.deltaTime - } - - if Input.isKeyPressed(.l) { - transform.position.y -= speed * context.deltaTime - } - - if Input.isKeyPressed(.o) { - transform.position.y += speed * context.deltaTime - } - - entity.components += transform - } - } - -// context.world.performQuery(Self.playerQuery).forEach { entity in -// let body = entity.components[PhysicsBody2DComponent.self]! -// -// if Input.isKeyPressed(.space) { -// body.applyLinearImpulse([0, 0.15], point: .zero, wake: true) +//import AdaEngine +// +//@MainActor +//struct GameScene2DPlugin: Plugin { +// +// @LocalIsolated +// private var textureAtlas: TextureAtlas! +// +// @LocalIsolated +// private var characterAtlas: TextureAtlas! +// +// @LocalIsolated +// private var disposeBag: Set = [] +// +// func setup(in app: AppWorlds) { +// do { +// let tiles = try AssetsManager.loadSync( +// Image.self, +// at: "@res://tiles_packed.png" +// ).asset! +// let charactersTiles = try AssetsManager.loadSync( +// Image.self, +// at: "@res://characters_packed.png" +// ).asset! +// +// self.textureAtlas = TextureAtlas(from: tiles, size: [18, 18]) +// self.characterAtlas = TextureAtlas(from: charactersTiles, size: [20, 23], margin: [4, 1]) +// } catch { +// fatalError(error.localizedDescription) +// } +// +// let cameraEntity = OrthographicCamera() +// cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) +// cameraEntity.camera.clearFlags = .solid +// cameraEntity.camera.orthographicScale = 1.1 +// app.main.addEntity(cameraEntity) +// +// self.makePlayer(app.main) +//// self.makeSubsceneAndSave(app) +// self.loadSubscene(app) +//// // try! self.makeCanvasItem(position: [-0.3, 0.4, -1]) +// self.collisionHandler(app) +//// +// app +// .addSystem(PlayerMovementSystem.self) +// .addSystem(SpawnPhysicsBodiesSystem.self) +// } +// +// private func collisionHandler(_ app: AppWorlds) { +// app.mainWorld.subscribe(to: CollisionEvents.Began.self) { event in +// if event.entityA.name == "Player" && (event.entityB.name == "Tube") { +// // event.entityA.scene?.removeEntity(event.entityA) +// // print("collide with tube") +// // self.gameOver() +// } +// } +// .store(in: &disposeBag) +// } +// +// private func makePlayer(_ world: World) { +// var transform = Transform() +// transform.scale = [0.2, 0.2, 0.2] +// +// let playerTexture = AnimatedTexture() +// playerTexture.framesPerSecond = 5 +// playerTexture.framesCount = 2 +// playerTexture[0] = self.characterAtlas[0, 0] +// playerTexture[1] = self.characterAtlas[1, 0] +// +// let playerEntity = Entity(name: "Player") +// playerEntity.components += SpriteComponent(texture: playerTexture) +// playerEntity.components += transform +// playerEntity.components += PhysicsBody2DComponent( +// shapes: [ +// .generateBox() +// ], +// mass: 1, +// mode: .kinematic +// ) +// playerEntity.components += PlayerComponent() +// world.addEntity(playerEntity) +// } +// +// private func makeSubsceneAndSave(_ app: AppWorlds) { +// let scene = Scene() +// +// var transform = Transform() +// transform.scale = [3, 0.19, 0.19] +// transform.position.y = -1 +// +// let untexturedEntity = Entity(name: "Ground") +// untexturedEntity.components += SpriteComponent(texture: self.textureAtlas[0, 0]) +// untexturedEntity.components += transform +// untexturedEntity.components += Collision2DComponent( +// shapes: [ +// .generateBox() +// ] +// ) +// +// transform.position.y = -1.5 +// +// let texturedEntity = Entity(name: "Ground 2") +// texturedEntity.components += SpriteComponent(tintColor: .red) +// texturedEntity.components += Transform() +// .setPosition(Vector3(0, 0.3, 0)) +// .setScale(Vector3(0.49, 0.49, 0.49)) +// texturedEntity.components += Collision2DComponent( +// shapes: [ +// .generateBox() +// ] +// ) +// +// scene.world.addEntity(untexturedEntity) +// scene.world.addEntity(texturedEntity) +// +// Task { +// try await AssetsManager.save(scene, at: "@res://", name: "Subscene.ascn") +// +// await MainActor.run { +// self.loadSubscene(app) +// } +// } +// } +// +// private func loadSubscene(_ app: AppWorlds) { +// Task { @MainActor in +// do { +// let scene = try await AssetsManager.load( +// Scene.self, +// at: "@res://Subscene.ascn", +// handleChanges: true +// ) +// app.mainWorld.addEntity( +// Entity(name: "Subscene") { +// DynamicScene(scene: scene) +// } +// ) +// } catch { +// print(error) +// } +// } +// } +//} +// +//@System(dependencies: [ +// .before(CameraSystem.self) +//]) +//struct PlayerMovementSystem { +// +// static let playerQuery = EntityQuery(where: .has(PlayerComponent.self) && .has(PhysicsBody2DComponent.self)) +// +// @Query, Ref, GlobalTransform> +// private var cameraQuery +// static let matQuery = EntityQuery(where: .has(Mesh2DComponent.self) && .has(Transform.self)) +// +// @Res +// private var deltaTime +// +// @Res +// private var input +// +// init(world: World) { } +// +// // swiftlint:disable:next function_body_length cyclomatic_complexity +// func update(context: UpdateContext) { +// cameraQuery.forEach { (camera, cameraTransform, globalTransform) in +// let speed: Float = 2 * deltaTime.deltaTime +// +// // --- Gamepad camera movement --- +// if let gamepad = input.getConnectedGamepads().first { +// let leftStickX = gamepad.getAxisValue(.leftStickX) +// let leftStickY = gamepad.getAxisValue(.leftStickY) +// let deadzone: Float = 0.1 +// if abs(leftStickX) > deadzone { +// cameraTransform.position.x += leftStickX * speed +// } +// if abs(leftStickY) > deadzone { +// cameraTransform.position.y += leftStickY * speed // Invert Y for typical 2D controls +// } +// +// let rightStickY = gamepad.getAxisValue(.rightStickY) +// if abs(rightStickY) > deadzone { +// camera.orthographicScale -= rightStickY * speed // Invert Y for typical 2D controls +// } +// } +// // --- End gamepad camera movement --- +// +// if Input.isKeyPressed(.w) { +// cameraTransform.position.y += speed +// } +// +// if Input.isKeyPressed(.s) { +// cameraTransform.position.y -= speed +// } +// +// if Input.isKeyPressed(.a) { +// cameraTransform.position.x -= speed +// } +// +// if Input.isKeyPressed(.d) { +// cameraTransform.position.x += speed +// } +// +// if Input.isKeyPressed(.arrowUp) { +// camera.orthographicScale -= speed +// } +// +// if Input.isKeyPressed(.arrowDown) { +// camera.orthographicScale += speed +// } +// +// context.world.performQuery(Self.matQuery).forEach { entity in +// let meshComponent = entity.components[Mesh2DComponent.self]! +// if Input.isMouseButtonPressed(.left) { +// (meshComponent.materials[0] as? CustomMaterial)?.color = .mint +// } else { +// (meshComponent.materials[0] as? CustomMaterial)?.color = .pink +// } +// +// (meshComponent.materials[0] as? CustomMaterial)?.time += context.deltaTime +// +// var transform = entity.components[Transform.self]! +// +// if Input.isMouseButtonPressed(.left) { +// let mousePosition = Input.getMousePosition() +// if let position = camera.wrappedValue.viewportToWorld2D(cameraGlobalTransform: globalTransform.matrix, viewportPosition: mousePosition) { +// // let values = context.scene.physicsWorld2D?.raycast(from: .zero, to: position) +// +// transform.position.x = position.x +// transform.position.y = -position.y +// } +// } +// +// let speed: Float = 3 +// +// if Input.isKeyPressed(.semicolon) { +// transform.position.x += speed * context.deltaTime +// } +// +// if Input.isKeyPressed(.k) { +// transform.position.x -= speed * context.deltaTime +// } +// +// if Input.isKeyPressed(.l) { +// transform.position.y -= speed * context.deltaTime +// } +// +// if Input.isKeyPressed(.o) { +// transform.position.y += speed * context.deltaTime +// } +// +// entity.components += transform +// } +// } +// +//// context.world.performQuery(Self.playerQuery).forEach { entity in +//// let body = entity.components[PhysicsBody2DComponent.self]! +//// +//// if Input.isKeyPressed(.space) { +//// body.applyLinearImpulse([0, 0.15], point: .zero, wake: true) +//// } +//// +//// for touch in Input.getTouches() where touch.phase == .began { +//// body.applyLinearImpulse([0, 0.15], point: .zero, wake: true) +//// } +//// +//// if Input.isKeyPressed(.arrowLeft) { +//// body.applyLinearImpulse([-0.05, 0], point: .zero, wake: true) +//// } +//// +//// if Input.isKeyPressed(.arrowRight) { +//// body.applyLinearImpulse([0.05, 0], point: .zero, wake: true) +//// } +//// } +// } +//} +// +//final class PlayerComponent: ScriptableComponent, @unchecked Sendable { +// +// @RequiredComponent var body: PhysicsBody2DComponent +// +// override func onUpdate(_ deltaTime: AdaEngine.TimeInterval) { +// if Input.isKeyPressed(.space) { +// body.applyLinearImpulse([0, 1], point: .zero, wake: true) +// } +// } +// +// override func onEvent(_ events: [any InputEvent]) { +// for event in events { +// if let touch = event as? TouchEvent { +// if touch.phase == .moved { +// body.applyLinearImpulse([0, 1], point: .zero, wake: true) +// } // } +// } +// } +//} +// +//// @Component +//// struct PlayerComponent { } // -// for touch in Input.getTouches() where touch.phase == .began { -// body.applyLinearImpulse([0, 0.15], point: .zero, wake: true) +//@System +//struct SpawnPhysicsBodiesSystem { +// +// @Query +// private var camera +// let fixedTimestep: FixedTimestep = FixedTimestep(stepsPerSecond: 20) +// +// init(world: World) { } +// +// func update(context: UpdateContext) { +// let result = fixedTimestep.advance(with: context.deltaTime) +// if !result.isFixedTick { +// return +// } +// +// self.camera.forEach { camera, globalTransform in +// if Input.isMouseButtonPressed(.left) { +// let mousePosition = Input.getMousePosition() +// if let position = camera.viewportToWorld2D(cameraGlobalTransform: globalTransform.matrix, viewportPosition: mousePosition) { +// self.spawnPhysicsBody(at: Vector3(position.x, -position.y, 1), world: context.world) +// } // } // -// if Input.isKeyPressed(.arrowLeft) { -// body.applyLinearImpulse([-0.05, 0], point: .zero, wake: true) +// if let gamepad = Input.getConnectedGamepads().first, gamepad.isGamepadButtonPressed(.rightTriggerButton) { +// let centerOfScreen = Vector2(camera.viewport!.rect.width / 2, camera.viewport!.rect.height / 2) +// if let position = camera.viewportToWorld2D(cameraGlobalTransform: globalTransform.matrix, viewportPosition: centerOfScreen) { +// self.spawnPhysicsBody(at: Vector3(position.x, -position.y, 1), world: context.world) +// } // } +// } +// } +// +// private func spawnPhysicsBody(at position: Vector3, world: World) { +// let isCircle = Input.isKeyPressed(.space) // -// if Input.isKeyPressed(.arrowRight) { -// body.applyLinearImpulse([0.05, 0], point: .zero, wake: true) +// let entity = Entity { +// PhysicsBody2DComponent( +// shapes: [ +// isCircle ? .generateCircle(radius: 1) : .generateBox() +// ], +// mass: 1, +// mode: .dynamic +// ) +// +// Transform(scale: Vector3(0.4), position: position) +// +// if isCircle { +// Circle2DComponent(color: .red) +// } else { +// SpriteComponent(tintColor: .blue) // } // } - } -} - -final class PlayerComponent: ScriptableComponent, @unchecked Sendable { - - @RequiredComponent var body: PhysicsBody2DComponent - - override func onUpdate(_ deltaTime: AdaEngine.TimeInterval) { - if Input.isKeyPressed(.space) { - body.applyLinearImpulse([0, 1], point: .zero, wake: true) - } - } - - override func onEvent(_ events: [any InputEvent]) { - for event in events { - if let touch = event as? TouchEvent { - if touch.phase == .moved { - body.applyLinearImpulse([0, 1], point: .zero, wake: true) - } - } - } - } -} - -// @Component -// struct PlayerComponent { } - -struct MyMaterial: CanvasMaterial { - - @Uniform(binding: 2, propertyName: "u_Time") - var time: Float - - @Uniform(binding: 2, propertyName: "u_Color") - var color: Color - - @FragmentTexture(binding: 0) - var customTexture: Texture2D - - init(color: Color, customTexture: Texture2D) { - self.time = 0 - self.color = color - self.customTexture = customTexture - } - - static func fragmentShader() throws -> AssetHandle { - try AssetsManager.loadSync( - ShaderSource.self, - at: "Assets/custom_material.glsl", - from: .editor - ) - } -} - -@System -struct SpawnPhysicsBodiesSystem { - - @Query - private var camera - let fixedTimestep: FixedTimestep = FixedTimestep(stepsPerSecond: 20) - - init(world: World) { } - - func update(context: inout UpdateContext) { - let result = fixedTimestep.advance(with: context.deltaTime) - if !result.isFixedTick { - return - } - - self.camera.forEach { camera, globalTransform in - if Input.isMouseButtonPressed(.left) { - let mousePosition = Input.getMousePosition() - if let position = camera.viewportToWorld2D(cameraGlobalTransform: globalTransform.matrix, viewportPosition: mousePosition) { - self.spawnPhysicsBody(at: Vector3(position.x, -position.y, 1), world: context.world) - } - } - - if let gamepad = Input.getConnectedGamepads().first, gamepad.isGamepadButtonPressed(.rightTriggerButton) { - let centerOfScreen = Vector2(camera.viewport!.rect.width / 2, camera.viewport!.rect.height / 2) - if let position = camera.viewportToWorld2D(cameraGlobalTransform: globalTransform.matrix, viewportPosition: centerOfScreen) { - self.spawnPhysicsBody(at: Vector3(position.x, -position.y, 1), world: context.world) - } - } - } - } - - private func spawnPhysicsBody(at position: Vector3, world: World) { - let isCircle = Input.isKeyPressed(.space) - - let entity = Entity { - PhysicsBody2DComponent( - shapes: [ - isCircle ? .generateCircle(radius: 1) : .generateBox() - ], - mass: 1, - mode: .dynamic - ) - - Transform(scale: Vector3(0.4), position: position) - - if isCircle { - Circle2DComponent(color: .red) - } else { - SpriteComponent(tintColor: .blue) - } - } - - world.addEntity(entity) - } -} +// +// world.addEntity(entity) +// } +//} diff --git a/Sources/AdaEditor/Scenes/SpaceInvaders.swift b/Sources/AdaEditor/Scenes/SpaceInvaders.swift deleted file mode 100644 index 14e560306..000000000 --- a/Sources/AdaEditor/Scenes/SpaceInvaders.swift +++ /dev/null @@ -1,416 +0,0 @@ -// -// SpaceInvaders.swift -// AdaEngine -// -// Created by v.prusakov on 4/6/23. -// - -import AdaEngine - -/// FIXME: This scene has a bug with animated texture. They doesn't dispawned correctly.. - -class SpaceInvaders: Scene, @unchecked Sendable { - - var disposeBag: Set = [] - var characterAtlas: TextureAtlas! - - override func sceneDidMove(to view: SceneView) { -// let sound = try! AssetsManager.loadSync("Assets/WindlessSlopes.wav", from: Bundle.editor) as AudioResource - let charactersTiles = try! AssetsManager.loadSync( - Image.self, - at: "Assets/characters_packed.png", - from: Bundle.editor - ).asset - self.characterAtlas = TextureAtlas(from: charactersTiles, size: [20, 23], margin: [4, 1]) - - let camera = OrthographicCamera() - camera.camera.clearFlags = .solid - camera.camera.backgroundColor = .black -// camera.prepareAudio(sound) -// .setLoop(true) -// .setVolume(0.6) -// .play() - - self.world.addEntity(camera) - - try! self.makePlayer() - try! self.makeScore() - - // Systems - - self.world.addSystem(MovementSystem.self) - self.world.addSystem(FireSystem.self) - self.world.addSystem(BulletSystem.self) - - self.world.addSystem(EnemySpawnerSystem.self) - self.world.addSystem(EnemyMovementSystem.self) - self.world.addSystem(EnemyLifetimeSystem.self) - self.world.addSystem(EnemyExplosionSystem.self) - - self.world.addSystem(ScoreSystem.self) - - self.subscribe(to: CollisionEvents.Began.self) { event in - if let bullet = event.entityB.components[Bullet.self], var enemy = event.entityA.components[EnemyComponent.self] { - enemy.health -= bullet.damage - - event.entityA.components += enemy - event.entityB.removeFromScene() - } - } - .store(in: &self.disposeBag) - - self.subscribe(to: SceneEvents.OnReady.self) { event in - Task { @MainActor in - event.scene.world.physicsWorld2D?.gravity = .zero - } - }.store(in: &self.disposeBag) - } - - private func makePlayer() throws { - let player = Entity() - - player.components += Transform(scale: Vector3(0.2), position: [0, -0.85, 0]) - player.components += PlayerComponent() - player.components += SpriteComponent(texture: characterAtlas[7, 1]) - - self.world.addEntity(player) - } - - private func makeScore() throws { - var container = TextAttributeContainer() - container.foregroundColor = .white - let attributedText = AttributedText("Score: 0", attributes: container) - - let score = Entity(name: "Score") { - Text2DComponent(text: attributedText) - GameState() - Transform(scale: Vector3(0.1), position: [-0.2, -0.9, 0]) - NoFrustumCulling() - } - - self.world.addEntity(score) - } -} - -struct MovementSystem: System { - - static let camera = EntityQuery(where: .has(Camera.self)) - static let player = EntityQuery(where: .has(PlayerComponent.self)) - - init(world: World) { } - - func update(context: inout UpdateContext) { - let cameraEntity = context.world.performQuery(Self.camera).first! - let camera = cameraEntity.components[Camera.self]! - guard let globalTransform = cameraEntity.components[GlobalTransform.self]?.matrix else { - return - } - - let mousePosition = Input.getMousePosition() - - let worldPosition = camera.viewportToWorld2D(cameraGlobalTransform: globalTransform, viewportPosition: mousePosition) ?? .zero - - context.world.performQuery(Self.player).forEach { entity in - var transform = entity.components[Transform.self]! - transform.position.x = worldPosition.x - transform.position.y = -worldPosition.y - - entity.components += transform - } - } -} - -struct FireSystem: System { - - static let player = EntityQuery(where: .has(PlayerComponent.self)) - - let fixedTime = FixedTimestep(stepsPerSecond: 12) - let laserAudio: AudioResource - - init(world: World) { - self.laserAudio = try! AssetsManager.loadSync( - AudioResource.self, - at: "Assets/laserShoot.wav", - from: .editor - ).asset - } - - func update(context: inout UpdateContext) { - context.world.performQuery(Self.player).forEach { entity in - let transform = entity.components[Transform.self]! - - if Input.isMouseButtonPressed(.left) || Input.isKeyPressed(.space) { - - let result = fixedTime.advance(with: context.deltaTime) - - if result.isFixedTick { - let controller = entity.prepareAudio(self.laserAudio) - - if controller.isPlaying { - controller.stop() - } - - controller.volume = 0.15 - controller.play() - - fireBullet(context: context, shipTransform: transform) - } - } - } - } - - func fireBullet(context: borrowing UpdateContext, shipTransform: Transform) { - let bullet = Entity(name: "Bullet") - - let bulletScale = Vector3(0.02, 0.04, 0.04) - - bullet.components += Transform(scale: bulletScale, position: shipTransform.position) - bullet.components += SpriteComponent(tintColor: .red) - bullet.components += Bullet(lifetime: 4) - - var collision = PhysicsBody2DComponent( - shapes: [ - .generateBox() - ], - mass: 1, - mode: .dynamic - ) - - collision.filter.categoryBitMask = .bullet - bullet.components += collision - context.world.addEntity(bullet) - } -} - -@Component -struct Bullet { - var damage: Float = 30 - let lifetime: Float - var currentLifetime: Float = 0 -} - -struct BulletSystem: System { - - static let bullet = EntityQuery(where: .has(Bullet.self) && .has(PhysicsBody2DComponent.self)) - static let bulletSpeed: Float = 3 - - init(world: World) { } - - func update(context: inout UpdateContext) { - context.world.performQuery(Self.bullet).forEach { entity in - var (bullet, body) = entity.components[Bullet.self, PhysicsBody2DComponent.self] - - body.linearVelocity = [0, Self.bulletSpeed] - bullet.currentLifetime += context.deltaTime - - if bullet.lifetime > bullet.currentLifetime { - entity.components += bullet - } else { - entity.removeFromScene() - } - } - } -} - -@Component -struct EnemyComponent { - var health: Float - let lifetime: Float - var currentLifetime: Float = 0 -} - -struct EnemySpawnerSystem: System { - - let fixedTime = FixedTimestep(stepsPerSecond: 2) - - let textureAtlas: TextureAtlas - - init(world: World) { - do { - let tiles = try AssetsManager.loadSync( - Image.self, - at: "Assets/tiles_packed.png", - from: Bundle.editor - ).asset - - self.textureAtlas = TextureAtlas(from: tiles, size: [18, 18]) - } catch { - fatalError(error.localizedDescription) - } - } - - func update(context: inout UpdateContext) { - let result = fixedTime.advance(with: context.deltaTime) - - if result.isFixedTick { - self.spawnEnemy(context: context) - } - } - - func spawnEnemy(context: borrowing UpdateContext) { - let entity = Entity(name: "Enemy") - - var transform = Transform() - transform.scale = Vector3(0.25) - transform.position = [Float.random(in: -1.8...1.8), 1, -1] - entity.components += transform - entity.components += SpriteComponent(texture: textureAtlas[5, 7]) - - var collision = Collision2DComponent( - shapes: [ - .generateBox() - ], - mode: .trigger - ) - - collision.filter.collisionBitMask = .bullet - - entity.components += collision - entity.components += EnemyComponent(health: 100, lifetime: 12) - context.world.addEntity(entity) - } -} - -struct EnemyLifetimeSystem: System { - static let enemy = EntityQuery(where: .has(EnemyComponent.self) && .has(Transform.self)) - - init(world: World) { } - - func update(context: inout UpdateContext) { - context.world.performQuery(Self.enemy).forEach { entity in - var enemy = entity.components[EnemyComponent.self]! - - enemy.currentLifetime += context.deltaTime - - if enemy.lifetime > enemy.currentLifetime { - entity.components += enemy - } else { - entity.removeFromScene() - } - } - } -} - -struct EnemyMovementSystem: System { - - static let enemy = EntityQuery(where: .has(EnemyComponent.self) && .has(Transform.self)) - static let speed: Float = 0.1 - - init(world: World) { } - - func update(context: inout UpdateContext) { - context.world.performQuery(Self.enemy).forEach { entity in - var transform = entity.components[Transform.self]! - transform.position.y -= Self.speed * context.deltaTime - entity.components += transform - } - } -} - -extension CollisionGroup { - static let bullet = CollisionGroup(rawValue: 1 << 2) -} - -@Component -struct ExplosionComponent { } - -struct EnemyExplosionSystem: System { - - let exposionAtlas: TextureAtlas - let explosionAudio: AudioResource - - init(world: World) { - do { - let image = try AssetsManager.loadSync( - Image.self, - at: "Assets/explosion.png", - from: .editor - ).asset - self.exposionAtlas = TextureAtlas(from: image, size: SizeInt(width: 32, height: 32)) - - self.explosionAudio = try AssetsManager.loadSync( - AudioResource.self, - at: "Assets/explosion-1.wav", - from: .editor - ).asset - } catch { - fatalError("Can't load assets \(error)") - } - } - - static let enemy = EntityQuery(where: .has(EnemyComponent.self) && .has(Transform.self)) - static let explosions = EntityQuery(where: .has(ExplosionComponent.self)) - static let scores = EntityQuery(where: .has(GameState.self)) - - func update(context: inout UpdateContext) { - let scores = context.world.performQuery(Self.scores).first - - // Make expolosions - context.world.performQuery(Self.enemy).forEach { entity in - let (enemy, transform) = entity.components[EnemyComponent.self, Transform.self] - - if enemy.health <= 0 { - scores?.components[GameState.self]?.score += 1 - - let texture = AnimatedTexture() - texture.framesPerSecond = 6 - texture.framesCount = 6 - texture.options = [] - texture[0] = self.exposionAtlas[0, 0] - texture[1] = self.exposionAtlas[1, 0] - texture[2] = self.exposionAtlas[2, 0] - texture[3] = self.exposionAtlas[3, 0] - texture[4] = self.exposionAtlas[4, 0] - texture[5] = self.exposionAtlas[5, 0] - - let explosion = Entity() - let controller = explosion.prepareAudio(self.explosionAudio) - controller.volume = 0.4 - controller.play() - - explosion.components += SpriteComponent(texture: texture) - explosion.components += transform - explosion.components += ExplosionComponent() - context.world.addEntity(explosion) - - entity.removeFromScene() - } - } - - // Remove explosions - context.world.performQuery(Self.explosions).forEach { entity in - guard let texture = entity.components[SpriteComponent.self]?.texture as? AnimatedTexture else { - return - } - - if texture.isPaused { - entity.removeFromScene() - } - } - } -} - -@Component -struct GameState { - var score: Int = 0 -} - -struct ScoreSystem: System { - - static let scores = EntityQuery(where: .has(Text2DComponent.self) && .has(GameState.self)) - - var container: TextAttributeContainer - - init(world: World) { - self.container = TextAttributeContainer() - self.container.foregroundColor = .white - } - - func update(context: inout UpdateContext) { - for entity in context.world.performQuery(Self.scores) { - var (text, score) = entity.components[Text2DComponent.self, GameState.self] - text.text = AttributedText("Score: \(score.score)", attributes: self.container) - entity.components += text - } - } -} diff --git a/Sources/AdaEditor/Scenes/TilemapScene.swift b/Sources/AdaEditor/Scenes/TilemapScene.swift deleted file mode 100644 index 2253ed838..000000000 --- a/Sources/AdaEditor/Scenes/TilemapScene.swift +++ /dev/null @@ -1,300 +0,0 @@ -// -// TilemapScene.swift -// -// -// Created by v.prusakov on 5/4/24. -// - -import AdaEngine - -final class LdtkTilemapScene: Scene, TileMapDelegate, @unchecked Sendable { - override func sceneDidMove(to view: SceneView) { - - let cameraEntity = OrthographicCamera() - cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) - cameraEntity.camera.clearFlags = .solid - cameraEntity.camera.orthographicScale = 10.5 - - self.world.addEntity(cameraEntity) - - var transform = Transform() - transform.position.y = -0.5 - transform.scale = Vector3(0.5) - - do { - let tileMap = try AssetsManager.loadSync( - LDtk.TileMap.self, - at: "Assets/TestTileMap.ldtk", - from: .editor - ).asset - tileMap.delegate = self - tileMap.loadLevel(at: 0) - - let tilemapEnt = Entity { - TileMapComponent(tileMap: tileMap) - NoFrustumCulling() - transform - } - - self.world.addEntity(tilemapEnt) - } catch { - fatalError("Failed to load \(error)") - } - - self.world.addSystem(CamMovementSystem.self) - } - - // MARK: - LDtk.EntityTileSourceDelegate - - func tileMap(_ tileMap: LDtk.TileMap, needsUpdate entity: Entity, from instance: LDtk.EntityInstance, in tileSource: LDtk.EntityTileSource) { - - } -} - -final class TilemapScene: Scene, @unchecked Sendable { - - enum TileAtlasCoordinates { - static let topLeft: PointInt = [1, 5] - static let topRight: PointInt = [3, 5] - static let bottomLeft: PointInt = [1, 7] - static let bottomRight: PointInt = [3, 7] - static let middleTop: PointInt = [2, 5] - static let middleBottom: PointInt = [2, 7] - - static let first: PointInt = [1, 6] - static let last: PointInt = [3, 6] - static let plain: PointInt = [2, 6] - - static let riverStart: PointInt = [14, 1] - static let riverBody: PointInt = [14, 2] - static let riverEnd: PointInt = [14, 3] - } - - override func sceneDidMove(to view: SceneView) { -// if FileSystem.current.itemExists(at: URL(filePath: "/Users/vprusakov/Downloads/tilemap.res")) { -// loadIfNeeded() -// } else { - save() -// } - } - - private func loadIfNeeded() { - let tileMap = try! AssetsManager.loadSync( - TileMap.self, - at: "/Users/vprusakov/Downloads/tilemap.res", - from: .editor - ).asset - - let cameraEntity = OrthographicCamera() - cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) - cameraEntity.camera.clearFlags = .solid - cameraEntity.camera.orthographicScale = 1.5 - - self.world.addEntity(cameraEntity) - - var transform = Transform() - transform.position.y = -0.5 - transform.scale = Vector3(0.5) - - let tilemapEnt = Entity { - TileMapComponent(tileMap: tileMap) - NoFrustumCulling() - transform - } - - self.world.addEntity(tilemapEnt) - self.world.addSystem(CamMovementSystem.self) - } - - // swiftlint:disable:next function_body_length - private func save() { - let tileMap = TileMap() - - let image = try! AssetsManager.loadSync( - Image.self, - at: "Assets/tiles_packed.png" - ).asset - let source = TextureAtlasTileSource(from: image, size: [18, 18]) - - source.createTile(for: TileAtlasCoordinates.topLeft) - source.createTile(for: TileAtlasCoordinates.topRight) - source.createTile(for: TileAtlasCoordinates.bottomLeft) - source.createTile(for: TileAtlasCoordinates.bottomRight) - source.createTile(for: TileAtlasCoordinates.middleTop) - source.createTile(for: TileAtlasCoordinates.middleBottom) - - source.createTile(for: TileAtlasCoordinates.last) - source.createTile(for: TileAtlasCoordinates.first) - source.createTile(for: TileAtlasCoordinates.plain) - - // Add animated river - source.createTile(for: TileAtlasCoordinates.riverStart) - .setAnimationFrameColumns(2) - .setAnimationFrameDuration(0.5) - - source.createTile(for: TileAtlasCoordinates.riverBody) - .setAnimationFrameColumns(2) - .setAnimationFrameDuration(0.5) - - source.createTile(for: TileAtlasCoordinates.riverEnd) - .setAnimationFrameColumns(2) - .setAnimationFrameDuration(0.5) - - let sourceId = tileMap.tileSet.addTileSource(source) - - let xRange = 0..<15 - let yRange = 0..<6 - - for x in xRange { - for y in yRange { - let atlasCoordinates = getCoordinates(for: x, y: y, maxX: xRange.upperBound, maxY: yRange.upperBound) - - tileMap.layers[0].setCell( - at: [x, y], - sourceId: sourceId, - atlasCoordinates: atlasCoordinates - ) - } - } - - for y in yRange { - let isStart = y == 0 - let isEnd = y == yRange.upperBound - 1 - - var coordinates: PointInt = [0, 0] - - if isStart { - coordinates = TileAtlasCoordinates.riverEnd - } else if isEnd { - coordinates = TileAtlasCoordinates.riverStart - } else { - coordinates = TileAtlasCoordinates.riverBody - } - - tileMap.layers[0].setCell( - at: [0, y], - sourceId: sourceId, - atlasCoordinates: coordinates - ) - } - - let cameraEntity = OrthographicCamera() - cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) - cameraEntity.camera.clearFlags = .solid - cameraEntity.camera.orthographicScale = 1.5 - - self.world.addEntity(cameraEntity) - - var transform = Transform() - transform.position.y = -0.5 - transform.scale = Vector3(0.5) - - let tilemapEnt = Entity { - TileMapComponent(tileMap: tileMap) - NoFrustumCulling() - transform - } - - self.world.addEntity(tilemapEnt) - self.world.addSystem(CamMovementSystem.self) - - Task { @AssetActor in - do { - try await AssetsManager.save(tileMap, at: "/Users/vprusakov/Downloads", name: "tilemap") - } catch { - print("Failed", error) - } - } - } - - func getCoordinates(for x: Int, y: Int, maxX: Int, maxY: Int) -> PointInt { - let isFirst = x == 0 - let isLast = x == maxX - 1 - - let bottom = y == 0 - let top = y == maxY - 1 - - if isFirst && top { - return TileAtlasCoordinates.topLeft - } - - if isFirst && bottom { - return TileAtlasCoordinates.bottomLeft - } - - if isLast && top { - return TileAtlasCoordinates.topRight - } - - if isLast && bottom { - return TileAtlasCoordinates.bottomRight - } - - if top { - return TileAtlasCoordinates.middleTop - } - - if bottom { - return TileAtlasCoordinates.middleBottom - } - - if isFirst { - return TileAtlasCoordinates.first - } - - if isLast { - return TileAtlasCoordinates.last - } - - return TileAtlasCoordinates.plain - } -} - -struct CamMovementSystem: System { - - static let cameraQuery = EntityQuery(where: .has(Camera.self) && .has(Transform.self)) - static let tileMap = EntityQuery(where: .has(TileMapComponent.self)) - - init(world: World) { } - - func update(context: inout UpdateContext) { - let cameraEntity: Entity = context.world.performQuery(Self.cameraQuery).first! -// let tileEntity: Entity = context.world.performQuery(Self.tileMap).first! - -// if Input.isKeyPressed(.m) { -// tileEntity.components[TileMapComponent.self]!.tileMap.layers[0].isEnabled.toggle() -// } - - var (camera, cameraTransform) = cameraEntity.components[Camera.self, Transform.self] - - let speed: Float = Input.isKeyPressed(.space) ? 5 : 2 - let speedNormalized: Float = speed * context.deltaTime - - if Input.isKeyPressed(.w) { - cameraTransform.position.y += speedNormalized - } - - if Input.isKeyPressed(.s) { - cameraTransform.position.y -= speedNormalized - } - - if Input.isKeyPressed(.a) { - cameraTransform.position.x -= speedNormalized - } - - if Input.isKeyPressed(.d) { - cameraTransform.position.x += speedNormalized - } - - if Input.isKeyPressed(.arrowUp) { - camera.orthographicScale -= speedNormalized - } - - if Input.isKeyPressed(.arrowDown) { - camera.orthographicScale += speedNormalized - } - - cameraEntity.components += cameraTransform - cameraEntity.components += camera - } -} diff --git a/Sources/AdaEditor/Scenes/TransformEntChildrenScene.swift b/Sources/AdaEditor/Scenes/TransformEntChildrenScene.swift deleted file mode 100644 index 60357924c..000000000 --- a/Sources/AdaEditor/Scenes/TransformEntChildrenScene.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// TransformEntChildrenScene.swift -// -// -// Created by v.prusakov on 5/4/24. -// - -import AdaEngine - -final class TransformEntChildrenScene: Scene, @unchecked Sendable { - - private var characterAtlas: TextureAtlas! - - override func sceneDidMove(to view: SceneView) { - let charactersTiles = try! AssetsManager.loadSync( - Image.self, - at: "Assets/characters_packed.png", - from: Bundle.editor - ).asset - self.characterAtlas = TextureAtlas(from: charactersTiles, size: [20, 23], margin: [4, 1]) - - let cameraEntity = OrthographicCamera() - cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) - cameraEntity.camera.clearFlags = .solid - cameraEntity.camera.orthographicScale = 1.5 - - self.world.addEntity(cameraEntity) - - let parent = Entity(name: "parent") { - Transform(scale: Vector3(0.5), position: [0, 0, 0]) - SpriteComponent(texture: characterAtlas[0, 0]) - ParentComponent() - - Collision2DComponent( - shapes: [.generateBox()], - mode: .trigger - ) - } - - let child = Entity(name: "child") { - Transform(scale: Vector3(0.4), position: [0.5, -0.5, 0]) - SpriteComponent(texture: characterAtlas[0, 1]) - Collision2DComponent( - shapes: [.generateBox(width: 0.4, height: 0.4)], - mode: .trigger - ) - } - - parent.addChild(child) - - self.world.addEntity(parent) - self.world.addEntity(child) - - self.world.addSystem(ParentMovementSystem.self) - } - -} - -@Component -struct ParentComponent {} - -class ParentMovementSystem: System { - - static let query = EntityQuery(where: .has(ParentComponent.self)) - - var time: TimeInterval = 0 - - required init(world: World) { } - - func update(context: inout UpdateContext) { - time += context.deltaTime - context.world.performQuery(Self.query).forEach { entity in - var transform = entity.components[Transform.self]! - transform.position.x = Float(Math.sin(time)) * 1 - entity.components += transform - } - } -} diff --git a/Sources/AdaEditor/Scenes/UITestScene.swift b/Sources/AdaEditor/Scenes/UITestScene.swift deleted file mode 100644 index a662f666f..000000000 --- a/Sources/AdaEditor/Scenes/UITestScene.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// UITesScene.swift -// AdaEngine -// -// Created by vladislav.prusakov on 19.08.2024. -// - -import AdaEngine - -struct SomeContent: View { - var body: some View { - VStack { - Color.blue - - Color.green - } - } -} - -final class UITestScene: Scene, @unchecked Sendable { - override func sceneDidMove(to view: SceneView) { - let cameraEntity = OrthographicCamera() - cameraEntity.camera.backgroundColor = Color(135/255, 206/255, 235/255, 1) - cameraEntity.camera.clearFlags = .solid - cameraEntity.camera.orthographicScale = 1.5 - self.world.addEntity(cameraEntity) - - let entity = Entity { - SpriteComponent(tintColor: .red) - Transform(scale: Vector3(0.5), position: [0.5, 0, 0]) - } - - self.world.addEntity(entity) - - let container = UIContainerView(rootView: Color.blue) - container.backgroundColor = .surfaceClearColor - container.frame = Rect(x: 0, y: 0, width: 200, height: 200) - view.addSubview(container) - } -} diff --git a/Sources/AdaEditor/UI/EditorWindow.swift b/Sources/AdaEditor/UI/EditorWindow.swift index 544f011dd..614f98c2f 100644 --- a/Sources/AdaEditor/UI/EditorWindow.swift +++ b/Sources/AdaEditor/UI/EditorWindow.swift @@ -15,7 +15,6 @@ class ViewModel { } struct NestedContent: View { - @State var innerColor: Color = .red var body: some View { @@ -28,55 +27,6 @@ struct NestedContent: View { } } -extension Text.Layout { - var runs: some RandomAccessCollection { - flatMap { line in - line - } - } - - var flattenedRuns: some RandomAccessCollection { - runs.flatMap { $0 } - } -} - -struct AnimatedSineWaveOffsetRender: TextRenderer { - - let timeOffset: Double // Time offset - - init(timeOffset: Double) { - self.timeOffset = timeOffset - } - - func draw(layout: Text.Layout, in context: inout UIGraphicsContext) { - let count = layout.flattenedRuns.count // Count all RunSlices in the text layout - let width = layout.first?.typographicBounds.rect.width ?? 0 // Get the width of the text line - let height = layout.first?.typographicBounds.rect.height ?? 0 // Get the height of the text line - // Iterate through each RunSlice and its index - for (index, glyph) in layout.flattenedRuns.enumerated() { - // Calculate the sine wave offset for the current character - let offset = animatedSineWaveOffset( - forCharacterAt: index, - amplitude: Double(height) / 2, // Set amplitude to half the line height - wavelength: Double(width), - phaseOffset: timeOffset, - totalCharacters: count - ) - // Create a copy of the context and translate it - var copy = context - copy.translateBy(x: 0, y: Float(offset)) - // Draw the current RunSlice in the modified context - copy.draw(glyph) - } - - func animatedSineWaveOffset(forCharacterAt index: Int, amplitude: Double, wavelength: Double, phaseOffset: Double, totalCharacters: Int) -> Double { - let x = Double(index) - let position = (x / Double(totalCharacters)) * wavelength - let radians = ((position + phaseOffset) / wavelength) * 2 * .pi - return Math.sin(radians) * amplitude - } - } -} struct SomeKey: PreferenceKey { static let defaultValue: String = "" @@ -107,7 +57,6 @@ struct CustomButtonStyle: ButtonStyle { } struct ContentView: View { - @State private var isAnimated: Bool = false var body: some View { @@ -130,7 +79,6 @@ struct ContentView: View { } class EditorWindow: UIWindow { - weak var inspectableView: LayoutInspectableView? override func windowDidReady() { @@ -145,12 +93,6 @@ class EditorWindow: UIWindow { let view = UIContainerView(rootView: ContentView()) view.autoresizingRules = [.flexibleWidth, .flexibleHeight] inspectableView.addSubview(view) - - // FIXME: SceneView doesnt render when UI does - // let scene = TilemapScene() - // let sceneView = SceneView(scene: scene, frame: Rect(origin: Point(x: 60, y: 60), size: Size(width: 250, height: 250))) - // sceneView.backgroundColor = .red - // self.addSubview(sceneView) } override func buildMenu(with builder: UIMenuBuilder) { @@ -177,7 +119,6 @@ class EditorWindow: UIWindow { } class LayoutInspectableView: UIView { - var speed: Float = 0.2 var pitch: Angle = Angle.radians(0) var yaw: Angle = Angle.radians(-90) @@ -219,8 +160,6 @@ class LayoutInspectableView: UIView { isViewMatrixDirty = false } - - self.handleView(deltaTime) } override func draw(with context: UIGraphicsContext) { @@ -242,35 +181,12 @@ class LayoutInspectableView: UIView { self.cameraTransform.origin += event.scrollDelta.y * sensitivity * speed * cameraFront self.isViewMatrixDirty = true - } - - private func handleView(_ deltaTime: TimeInterval) { - - if Input.isKeyPressed(.w) { - cameraTransform.origin += speed * cameraFront * deltaTime - self.isViewMatrixDirty = true - } - - if Input.isKeyPressed(.a) { - cameraTransform.origin -= cross(cameraFront, cameraUp).normalized * speed * deltaTime - self.isViewMatrixDirty = true - } - if Input.isKeyPressed(.d) { - cameraTransform.origin += cross(cameraFront, cameraUp).normalized * speed * deltaTime - self.isViewMatrixDirty = true - } - - if Input.isKeyPressed(.s) { - cameraTransform.origin -= speed * cameraFront * deltaTime - self.isViewMatrixDirty = true - } - - guard Input.isMouseButtonPressed(.left) else { + guard event.button == .left && event.phase != .began else { return } - let position = Input.getMousePosition() + let position = event.mousePosition var xoffset = position.x - self.lastMousePosition.x var yoffset = self.lastMousePosition.y - position.y self.lastMousePosition = position @@ -285,7 +201,7 @@ class LayoutInspectableView: UIView { if pitch.radians > 89.0 { pitch = 89.0 } else if(pitch.radians < -89.0) { - pitch = -89.0 + pitch = -89.0 } var direction = Vector3() @@ -297,4 +213,25 @@ class LayoutInspectableView: UIView { self.isViewMatrixDirty = true } + + override func onKeyPressed(_ event: Set) { + for key in event where key.status == .down { + switch key.keyCode { + case .w: + cameraTransform.origin += speed * cameraFront + self.isViewMatrixDirty = true + case .a: + cameraTransform.origin -= cross(cameraFront, cameraUp).normalized * speed + self.isViewMatrixDirty = true + case .d: + cameraTransform.origin += cross(cameraFront, cameraUp).normalized * speed + self.isViewMatrixDirty = true + case .s: + cameraTransform.origin -= speed * cameraFront + self.isViewMatrixDirty = true + default: + return + } + } + } } diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-2-1.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-2-1.png index 55fda12f9..58bfa4f4f 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-2-1.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-2-1.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-1.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-1.png index 9dc045db7..867a73a5b 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-1.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-1.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-2.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-2.png index 4a67a7da5..07d1ef527 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-2.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-2.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-3.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-3.png index 0ba16c35d..d2f4bb20f 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-3.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-3.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-4.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-4.png index e3d69102f..d9465bdcf 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-4.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-4.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-5.mov b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-5.mov index 6b115c3d0..1924c8afb 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-5.mov and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/CF2G-3-5.mov differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/first2dscene-intro.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/first2dscene-intro.png index fe990d414..7eb75e5cc 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/first2dscene-intro.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Arts/first2dscene-intro.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-2-player.swift b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-2-player.swift index 908d68489..8f74b6323 100644 --- a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-2-player.swift +++ b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-2-player.swift @@ -20,7 +20,7 @@ struct FirstScene: Plugin { @Component struct PlayerComponent {} -@PlainSystem +@System func PlayerMovement() { } diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-3-player.swift b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-3-player.swift index fcc334277..e67541cb2 100644 --- a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-3-player.swift +++ b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-3-player.swift @@ -22,7 +22,7 @@ struct FirstScene: Plugin { @Component struct PlayerComponent {} -@PlainSystem +@System func PlayerMovement() { } diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-4-player.swift b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-4-player.swift index 8f21d1ebd..256527a4d 100644 --- a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-4-player.swift +++ b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-4-player.swift @@ -22,9 +22,9 @@ struct FirstScene: Plugin { @Component struct PlayerComponent {} -@PlainSystem +@System func PlayerMovement( _ playerTransform: FIlterQuery, With> ) { -} \ No newline at end of file +} diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-5-player.swift b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-5-player.swift index fefc0d5fc..b10bacacd 100644 --- a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-5-player.swift +++ b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-5-player.swift @@ -9,10 +9,10 @@ struct FirstScene: Plugin { @Component struct PlayerComponent {} -@PlainSystem +@System func PlayerMovement( _ playerTransform: FIlterQuery, With>, - _ speed: LocalIsolated = 3.0 + _ speed: Local = 3.0 ) { -} \ No newline at end of file +} diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-6-player.swift b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-6-player.swift index d43b46f3c..90b09da75 100644 --- a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-6-player.swift +++ b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-6-player.swift @@ -9,13 +9,13 @@ struct FirstScene: Plugin { @Component struct PlayerComponent {} -@PlainSystem +@System func PlayerMovement( _ playerTransform: FIlterQuery, With>, - _ speed: LocalIsolated = 3.0, - _ deltaTime: ResQuery + _ speed: Local = 3.0, + _ deltaTime: Res ) { if Input.isKeyPressed(.w) { playerTransform.position.y += speed.wrappedValue * deltaTime.wrappedValue.deltaTime } -} \ No newline at end of file +} diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-7-player.swift b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-7-player.swift index ca319de43..865b31cde 100644 --- a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-7-player.swift +++ b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateFirst2DGame/Code/CF2G-4-7-player.swift @@ -9,11 +9,11 @@ class FirstScene: Scene { @Component struct PlayerComponent {} -@PlainSystem +@System func PlayerMovement( _ playerTransform: FIlterQuery, With>, - _ speed: LocalIsolated = 3.0, - _ deltaTime: ResQuery + _ speed: Local = 3.0, + _ deltaTime: Res ) { if Input.isKeyPressed(.w) { playerTransform.position.y += speed.wrappedValue * deltaTime.wrappedValue.deltaTime diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-1-1.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-1-1.png index 4c144a453..f8fd83fa7 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-1-1.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-1-1.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-1-2.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-1-2.png index 935ce2491..d9762f4b1 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-1-2.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-1-2.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-2.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-2.png index 333dc0143..a6201ee68 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-2.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-2.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-3.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-3.png index e622b9820..3b853e20f 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-3.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-3.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-4.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-4.png index 4730d1b10..41abf8fa8 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-4.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-4.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-5.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-5.png index 5d705470b..62e2b5cb8 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-5.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-5.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-6.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-6.png index 5db5278e0..696cb6b3e 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-6.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-6.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-8.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-8.png index ddb59e570..f4429ee72 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-8.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-8.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-9.png b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-9.png index 2e4e371e0..9b0eaccda 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-9.png and b/Sources/AdaEngine/AdaEngine.docc/Resources/CreateProject/Arts/CP-2-9.png differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Resources/EventListeners/Arts/EL-1.jpg b/Sources/AdaEngine/AdaEngine.docc/Resources/EventListeners/Arts/EL-1.jpg index 907b63fe7..9a85eccc5 100644 Binary files a/Sources/AdaEngine/AdaEngine.docc/Resources/EventListeners/Arts/EL-1.jpg and b/Sources/AdaEngine/AdaEngine.docc/Resources/EventListeners/Arts/EL-1.jpg differ diff --git a/Sources/AdaEngine/AdaEngine.docc/Tutorials/AdaEngine/tutorials/CreateFirst2DScene.tutorial b/Sources/AdaEngine/AdaEngine.docc/Tutorials/AdaEngine/tutorials/CreateFirst2DScene.tutorial index ff85045b5..6cdc9d218 100644 --- a/Sources/AdaEngine/AdaEngine.docc/Tutorials/AdaEngine/tutorials/CreateFirst2DScene.tutorial +++ b/Sources/AdaEngine/AdaEngine.docc/Tutorials/AdaEngine/tutorials/CreateFirst2DScene.tutorial @@ -170,7 +170,7 @@ Now we can update our player position. AdaEngine can handle keyboard inputs for us. So that we should listen to keyboard inputs and if some key is pressed, we should change player transform. @Step { - Let's add a new function parameter named `deltaTime` with `ResQuery` type. This a query to resource in a world. + Let's add a new function parameter named `deltaTime` with `Res` type. This a query to resource in a world. Using special ``Input`` object we can check if some key is pressed. Add `if` condition where we will listen to the `w` key. If the key is pressed, then add multiplication speed by delta time. We do this for smooth transition between frames. diff --git a/Sources/AdaEngine/BUILD.bazel b/Sources/AdaEngine/BUILD.bazel index eb1e308d5..faa1ad772 100644 --- a/Sources/AdaEngine/BUILD.bazel +++ b/Sources/AdaEngine/BUILD.bazel @@ -35,10 +35,12 @@ ada_engine_deps = [ "@swiftpkg_swift_collections//:BitCollections", "@swiftpkg_swift_log//:Logging", "@swiftpkg_yams//:Yams", - "@swiftpkg_miniaudio//:miniaudio", - "@swiftpkg_box2d//:box2d", + "//Modules/box2d:box2d", + "//Modules/SPIRV-Cross:SPIRV-Cross", + "//Modules/glslang:glslang", "//Modules/libpng:libpng", - "@swiftpkg_spirv_cross//:SPIRV-Cross", + "//Modules/msdf-atlas-gen:msdf-atlas-gen", + "//Modules/miniaudio:miniaudio", "//Sources/AtlasFontGenerator:AtlasFontGenerator", "//Sources/SPIRVCompiler", "//Sources/Math", diff --git a/Sources/AdaEngine/DefaultPlugins.swift b/Sources/AdaEngine/DefaultPlugins.swift index 219eebead..db9213503 100644 --- a/Sources/AdaEngine/DefaultPlugins.swift +++ b/Sources/AdaEngine/DefaultPlugins.swift @@ -6,23 +6,22 @@ // import AdaApp -import AdaECS import AdaAudio -import AdaText -import AdaTransform +import AdaECS import AdaInput -import AdaUI -import AdaPlatform -import AdaTilemap -import AdaSprite import AdaPhysics +import AdaPlatform import AdaScene +import AdaSprite +import AdaText +import AdaTilemap +import AdaTransform +import AdaUI import OrderedCollections /// Contains base configuration for any scene in the game. /// This plugins will applied for each scene in Ada application and should be passed once per scene. public struct DefaultPlugins: Plugin { - private var plugins: OrderedDictionary /// Initialize a new instance of `DefaultPlugins` with the given file path. @@ -30,14 +29,14 @@ public struct DefaultPlugins: Plugin { public init(filePath: StaticString = #filePath) { var plugins = OrderedDictionary() insertPlugin(AppPlatformPlugin(), into: &plugins) + insertPlugin(InputPlugin(), into: &plugins) insertPlugin(RenderWorldPlugin(), into: &plugins) insertPlugin(CameraPlugin(), into: &plugins) - insertPlugin(InputPlugin(), into: &plugins) insertPlugin(AssetsPlugin(filePath: filePath), into: &plugins) insertPlugin(VisibilityPlugin(), into: &plugins) insertPlugin(SpritePlugin(), into: &plugins) insertPlugin(Mesh2DPlugin(), into: &plugins) - insertPlugin(Text2DPlugin(), into: &plugins) + insertPlugin(TextPlugin(), into: &plugins) insertPlugin(ScenePlugin(), into: &plugins) insertPlugin(AudioPlugin(), into: &plugins) insertPlugin(UIPlugin(), into: &plugins) @@ -64,7 +63,7 @@ public struct DefaultPlugins: Plugin { /// Set a plugin. /// - Parameter plugin: The plugin to set. /// - Returns: A new instance of `DefaultPlugins` with the plugin set. - public func set(_ plugin: T) -> DefaultPlugins { + public func set(_ plugin: T) -> Self { var newValue = self insertPlugin(plugin, into: &newValue.plugins) return newValue @@ -73,7 +72,7 @@ public struct DefaultPlugins: Plugin { /// Disable a plugin. /// - Parameter plugin: The plugin to disable. /// - Returns: A new instance of `DefaultPlugins` with the plugin disabled. - public func disable(_ plugin: T.Type) -> DefaultPlugins { + public func disable(_ plugin: T.Type) -> Self { var newValue = self newValue.plugins[String(reflecting: T.self)] = nil return newValue diff --git a/Sources/AdaEngine/Exported.swift b/Sources/AdaEngine/Exported.swift index 3c52c7e29..ef2a62774 100644 --- a/Sources/AdaEngine/Exported.swift +++ b/Sources/AdaEngine/Exported.swift @@ -9,21 +9,25 @@ // Like example, user can use Math library if he/she use imported only `AdaEngine`. // Can be problem if swift deprecate this @_exported hack. -@_exported import Foundation -@_exported import Math @_exported import AdaApp -@_exported import AdaECS -@_exported import AdaUtils @_exported import AdaAssets @_exported import AdaAudio -@_exported import AdaTransform +@_exported import AdaECS +@_exported import AdaInput +@_exported import AdaPhysics @_exported import AdaRender -@_exported import AdaText -@_exported import AdaUI @_exported import AdaScene -@_exported import AdaInput -@_exported import AdaTilemap @_exported import AdaSprite -@_exported import AdaPhysics +@_exported import AdaText +@_exported import AdaTilemap +@_exported import AdaTransform +@_exported import AdaUI +@_exported import AdaUtils +#if canImport(FoundationEssentials) +@_exported import FoundationEssentials +#else +@_exported import Foundation +#endif +@_exported import Math public typealias TimeInterval = AdaUtils.TimeInterval diff --git a/Sources/AdaEngine/Utils/Bundle+AdaEngine.swift b/Sources/AdaEngine/Utils/Bundle+AdaEngine.swift index 73df80102..c25408a96 100644 --- a/Sources/AdaEngine/Utils/Bundle+AdaEngine.swift +++ b/Sources/AdaEngine/Utils/Bundle+AdaEngine.swift @@ -5,12 +5,18 @@ // Created by vladislav.prusakov on 13.03.2025. // -public extension Bundle { - static var engineBundle: Bundle { +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public extension Foundation.Bundle { + static var engineBundle: Foundation.Bundle { #if SWIFT_PACKAGE && !BAZEL_BUILD - return Bundle.module + return Foundation.Bundle.module #else - return Bundle(for: BundleToken.self) + return Foundation.Bundle(for: BundleToken.self) #endif } } diff --git a/Sources/AdaEngine/Utils/FPSCounter.swift b/Sources/AdaEngine/Utils/FPSCounter.swift index 2d402d708..469250292 100644 --- a/Sources/AdaEngine/Utils/FPSCounter.swift +++ b/Sources/AdaEngine/Utils/FPSCounter.swift @@ -7,20 +7,19 @@ import Math -class FPSCounter { - - nonisolated(unsafe) static let shared = FPSCounter() - +public class FPSCounter { private var lastNotificationTime: LongTimeInterval = 0 private var notificationDelay: TimeInterval = 1.0 private var numberOfFrames = 0 - - func stop() { + + public init() { } + + public func stop() { self.lastNotificationTime = 0 self.numberOfFrames = 0 } - func tick() { + public func tick() { if self.lastNotificationTime == 0.0 { self.lastNotificationTime = Time.absolute return @@ -41,6 +40,7 @@ class FPSCounter { private func notifyUpdateForElapsedTime(_ elapsedTime: TimeInterval) { let rounded = Math.round(Double(self.numberOfFrames) / Double(elapsedTime)) let fps = Int(rounded) + print(fps) EventManager.default.send(EngineEvents.FramesPerSecondEvent(framesPerSecond: fps)) } } diff --git a/Sources/AdaEngineEmbeddable/ApplePlatforms/AEView.swift b/Sources/AdaEngineEmbeddable/ApplePlatforms/AEView.swift index 548ac2b3c..db55549ee 100644 --- a/Sources/AdaEngineEmbeddable/ApplePlatforms/AEView.swift +++ b/Sources/AdaEngineEmbeddable/ApplePlatforms/AEView.swift @@ -21,12 +21,6 @@ public final class AEView: MetalView { /// You can grab information about all views. public let engineWindow: AdaEngine.UIWindow - /// Create AEView with game scene. - public convenience init(scene: Scene, frame: CGRect) throws { - let sceneView = SceneView(scene: scene, frame: frame.toEngineRect) - try self.init(view: sceneView, frame: frame) - } - /// Create AEView with AdaEngine.View. public init(view: UIView, frame: CGRect) throws { let rect = frame.toEngineRect @@ -48,12 +42,14 @@ public final class AEView: MetalView { Application.shared.appleWindowManager.nativeView = self self.delegate = self - do { - try appContext.run() - try RenderEngine.shared.createWindow(.windowId(window.id), for: self, size: rect.size.toSizeInt()) - try AudioServer.shared.start() - } catch { - print("[AEView Error]", error.localizedDescription) + Task { @MainActor in + do { + try await appContext.run() + try RenderEngine.shared.createWindow(window.id, for: self, size: rect.size.toSizeInt()) + try AudioServer.shared.start() + } catch { + print("[AEView Error]", error.localizedDescription) + } } } @@ -76,7 +72,7 @@ extension AEView: MTKViewDelegate { public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { do { self.engineWindow.frame.size = size.toEngineSize - try RenderEngine.shared.resizeWindow(.windowId(self.engineWindow.id), newSize: size.toEngineSize.toSizeInt()) + try RenderEngine.shared.resizeWindow(self.engineWindow.id, newSize: size.toEngineSize.toSizeInt()) } catch { print("[AEView Error]", error.localizedDescription) } @@ -104,5 +100,4 @@ private extension Application { self.windowManager as! AppleWindowManager } } - #endif diff --git a/Sources/AdaEngineMacros/AdaEngineMacrosPlugin.swift b/Sources/AdaEngineMacros/AdaEngineMacrosPlugin.swift index d8b3f035f..e7ec6e758 100644 --- a/Sources/AdaEngineMacros/AdaEngineMacrosPlugin.swift +++ b/Sources/AdaEngineMacros/AdaEngineMacrosPlugin.swift @@ -13,6 +13,7 @@ struct AdaEngineMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ ComponentMacro.self, EntryMacro.self, - SystemMacro.self + SystemMacro.self, + BundleMacro.self ] } diff --git a/Sources/AdaEngineMacros/BundleMacro.swift b/Sources/AdaEngineMacros/BundleMacro.swift new file mode 100644 index 000000000..aace163c6 --- /dev/null +++ b/Sources/AdaEngineMacros/BundleMacro.swift @@ -0,0 +1,74 @@ +// +// ComponentMacro.swift +// AdaEngineMacros +// +// Created by v.prusakov on 2/14/24. +// + +import SwiftDiagnostics +import SwiftOperators +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacroExpansion +import SwiftSyntaxMacros + +public struct BundleMacro: MemberMacro, ExtensionMacro { + // Generate the 'components' property + public static func expansion< + D: DeclGroupSyntax, + C: MacroExpansionContext + >( + of node: AttributeSyntax, + providingMembersOf declaration: D, + in context: C + ) throws -> [DeclSyntax] { + guard let structDecl = declaration.as(StructDeclSyntax.self) else { + throw MacroError.macroUsage("Bundle macro can be applied only to structs.") + } + let availability = declaration.modifiers + // Collect all stored property names + let propertyNames: [String] = structDecl.memberBlock.members.compactMap { member in + guard let varDecl = member.decl.as(VariableDeclSyntax.self) else { return nil } + guard let binding = varDecl.bindings.first else { return nil } + guard let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { return nil } + // Only stored properties (not computed) + if binding.accessorBlock != nil { return nil } + return identifier + } + + // Generate the components property + let componentsProperty: DeclSyntax = """ + \(availability)var components: [any Component] { + [\(raw: propertyNames.joined(separator: ", "))] + } + """ + return [componentsProperty] + } + + // Generate the extension to conform to Bundle + public static func expansion< + D: DeclGroupSyntax, + T: TypeSyntaxProtocol, + C: MacroExpansionContext + >( + of node: AttributeSyntax, + attachedTo declaration: D, + providingExtensionsOf type: T, + conformingTo protocols: [TypeSyntax], + in context: C + ) throws -> [ExtensionDeclSyntax] { + // Only add conformance if not already present + if let inheritanceClause = declaration.inheritanceClause, + inheritanceClause.inheritedTypes.contains(where: { + ["ComponentsBundle", "AdaECS.ComponentsBundle"].contains($0.type.trimmedDescription) + }) { + return [] + } + + let ext: DeclSyntax = """ + extension \(type.trimmed): AdaECS.ComponentsBundle { } + """ + return [ext.cast(ExtensionDeclSyntax.self)] + } +} + diff --git a/Sources/AdaEngineMacros/SystemMacro.swift b/Sources/AdaEngineMacros/SystemMacro.swift index 012b624bc..1984beedb 100644 --- a/Sources/AdaEngineMacros/SystemMacro.swift +++ b/Sources/AdaEngineMacros/SystemMacro.swift @@ -13,23 +13,19 @@ import SwiftSyntaxMacroExpansion import SwiftSyntaxMacros public struct SystemMacro: MemberMacro { - public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { - // Find all properties with SystemQuery attribute + // Find all properties with SystemParameter attribute let entityQueries = declaration.memberBlock.members.compactMap { member -> String? in guard let varDecl = member.decl.as(VariableDeclSyntax.self) else { return nil } - let hasEntityQueryAttribute = varDecl.attributes.contains { attribute in - guard let attributeName = attribute.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { - return false - } - return attributeName.hasSuffix("Query") || attributeName == "Extract" + let hasPropertyWrapperAttribute = varDecl.attributes.contains { attribute in + return attribute.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self) != nil } - guard hasEntityQueryAttribute, + guard hasPropertyWrapperAttribute, let binding = varDecl.bindings.first, let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { return nil @@ -124,6 +120,17 @@ extension SystemMacro: PeerMacro { let funcName = funcDecl.name.text let params = funcDecl.signature.parameterClause.parameters let availability = funcDecl.modifiers + + // Check if function is async or has actor attributes + let isAsync = funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil + let hasActorAttribute = funcDecl.attributes.contains { attribute in + guard let attributeName = attribute.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.name.text else { + return false + } + return attributeName.hasSuffix("Actor") || attributeName == "MainActor" + } + + let needsAwait = isAsync || hasActorAttribute // Get dependencies from macro arguments var dependencies: [String] = [] @@ -148,7 +155,7 @@ extension SystemMacro: PeerMacro { // Generate property declarations and type list for queries var propertyDecls: [String] = [] var queryVars: [String] = [] - var paramNames: [(Bool, String)] = [] + var paramNames: [SystemInputParameter] = [] for param in params { let isAnonymosParam = param.firstName.text == "_" @@ -160,9 +167,28 @@ extension SystemMacro: PeerMacro { let defaultValue = param.defaultValue?.value.description let typeString = param.type.trimmedDescription - propertyDecls.append("@\(typeString)\nprivate var \(paramName)\(defaultValue != nil ? " = \(defaultValue!)" : "")") - queryVars.append("_\(paramName)") - paramNames.append((isAnonymosParam, paramName)) + + // Check for special types that shouldn't be added to propertyDecls + var specialType: SystemInputParameter.SpecialType = .none + if typeString.hasSuffix("WorldUpdateContext") || typeString.hasSuffix("UpdateContext") { + specialType = .context + } + if typeString.hasSuffix("World") || typeString.hasSuffix(".World") { + specialType = .world + } + + if specialType == .none { + propertyDecls.append("@\(typeString)\nprivate var \(paramName)\(defaultValue != nil ? " = \(defaultValue!)" : "")") + queryVars.append("_\(paramName)") + } + paramNames.append( + SystemInputParameter( + isAnonymosParam: isAnonymosParam, + isInoutParam: false, + paramName: paramName, + specialType: specialType + ) + ) } // Generate struct body @@ -172,8 +198,8 @@ extension SystemMacro: PeerMacro { \(availability)init(world: AdaECS.World) { } - \(availability)func update(context: inout UpdateContext) { - \(raw: funcName)(\(raw: paramNames.map { "\($0 ? "" : "\($1): ")_\($1)" }.joined(separator: ", "))) + \(availability)func update(context: UpdateContext)\(raw: needsAwait ? " async" : "") { + \(raw: needsAwait ? "await " : "")\(raw: funcName)(\(raw: paramNames.map { $0.buildParameter() }.joined(separator: ", "))) } \(availability) var queries: AdaECS.SystemQueries { @@ -185,4 +211,40 @@ extension SystemMacro: PeerMacro { """ return [structDecl] } + + struct SystemInputParameter { + enum SpecialType { + case world + case context + case commands + case none + } + + let isAnonymosParam: Bool + let isInoutParam: Bool + let paramName: String + let specialType: SpecialType + + func buildParameter() -> String { + let functionParam = if isAnonymosParam { + "" + } else { + "\(paramName): " + } + let propertyParam = if isInoutParam { + "&\(paramName)" + } else { + switch specialType { + case .world: + "context.\(paramName)" + case .context: + "context" + default: + "_\(paramName)" + } + } + + return "\(functionParam)\(propertyParam)" + } + } } diff --git a/Sources/AdaInput/AppleGameControllerManager.swift b/Sources/AdaInput/AppleGameControllerManager.swift index 62c0477c7..b84b2ead9 100644 --- a/Sources/AdaInput/AppleGameControllerManager.swift +++ b/Sources/AdaInput/AppleGameControllerManager.swift @@ -10,15 +10,15 @@ import AdaUtils import CoreHaptics import GameController -public final class AppleGameControllerManager { - - @MainActor - public static let shared = AppleGameControllerManager() +public final class AppleGameControllerManager: RumbleGameControllerEngine, @unchecked Sendable { private var knownGamepadIds: [GCController: Int] = [:] private var nextGamepadId: Int = 0 + private var receiveInputEvents: @Sendable (any InputEvent) -> Void - private init() {} + public init(receiveInputEvents: @escaping @Sendable (any InputEvent) -> Void) { + self.receiveInputEvents = receiveInputEvents + } // MARK: - Button and Axis Mapping Helpers @@ -155,8 +155,8 @@ public final class AppleGameControllerManager { time: TimeInterval(Date().timeIntervalSince1970) ) - Task { @MainActor in - Input.shared.receiveEvent(event) + Task { @MainActor [receiveInputEvents] in + receiveInputEvents(event) } } @@ -174,7 +174,7 @@ public final class AppleGameControllerManager { microGamepad.buttonMenu.pressedChangedHandler = { [weak self] button, pressure, pressed in self?.handleButtonChange(button: button, controller: controller, gamepadId: gamepadId, pressure: pressure, pressed: pressed) } - microGamepad.dpad.xAxis.valueChangedHandler = { [weak self] axis, value in + microGamepad.dpad.xAxis.valueChangedHandler = { [weak self, receiveInputEvents] axis, value in // Dpad X on microGamepad could be mapped to left/right buttons or an axis // For simplicity, sending as axis event first, then potentially button events. if let mappedAxis = self?.mapGCAxisToGamepadAxis(axis, controller: controller) { @@ -186,13 +186,13 @@ public final class AppleGameControllerManager { time: TimeInterval(Date().timeIntervalSince1970) ) Task { @MainActor in - Input.shared.receiveEvent(event) + receiveInputEvents(event) } } // Optionally, also simulate dpad left/right button presses based on value // This part can be complex due to thresholds and state management. } - microGamepad.dpad.yAxis.valueChangedHandler = { [weak self] axis, value in + microGamepad.dpad.yAxis.valueChangedHandler = { [weak self, receiveInputEvents] axis, value in // Similar for Dpad Y if let mappedAxis = self?.mapGCAxisToGamepadAxis(axis, controller: controller) { let event = GamepadAxisEvent( @@ -204,7 +204,7 @@ public final class AppleGameControllerManager { ) Task { @MainActor in - Input.shared.receiveEvent(event) + receiveInputEvents(event) } } } @@ -289,8 +289,8 @@ public final class AppleGameControllerManager { time: TimeInterval(Date().timeIntervalSince1970) ) - Task { @MainActor in - Input.shared.receiveEvent(event) + Task { @MainActor [receiveInputEvents] in + receiveInputEvents(event) } } @@ -313,8 +313,8 @@ public final class AppleGameControllerManager { time: TimeInterval(Date().timeIntervalSince1970) ) - Task { @MainActor in - Input.shared.receiveEvent(event) + Task { @MainActor [receiveInputEvents] in + receiveInputEvents(event) } } @@ -332,8 +332,8 @@ public final class AppleGameControllerManager { time: TimeInterval(Date().timeIntervalSince1970) ) - Task { @MainActor in - Input.shared.receiveEvent(event) + Task { @MainActor [receiveInputEvents] in + receiveInputEvents(event) } } @@ -355,8 +355,8 @@ public final class AppleGameControllerManager { window: .empty, time: TimeInterval(Date().timeIntervalSince1970) ) - Task { @MainActor in - Input.shared.receiveEvent(event) + Task { @MainActor [receiveInputEvents] in + receiveInputEvents(event) } self.knownGamepadIds.removeValue(forKey: controller) diff --git a/Sources/AdaInput/InputManager.swift b/Sources/AdaInput/InputManager.swift index c83f7f2ae..760cdb1bf 100644 --- a/Sources/AdaInput/InputManager.swift +++ b/Sources/AdaInput/InputManager.swift @@ -7,19 +7,22 @@ import AdaECS import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Math // - TODO: (Vlad) Add actions list and method like `isActionPressed` /// An object that contains inputs from keyboards, mouse, touch screens and etc. -public final class Input: Resource, @unchecked Sendable { +public struct Input: Resource, Sendable { - @_spi(Internal) - public static let shared = Input() @_spi(Internal) public var mousePosition: Point = .zero - private static let lock = NSLock() + private let lock = NSRecursiveLock() + @_spi(Internal) public private(set) var eventsPool: [any InputEvent] = [] // FIXME: (Vlad) Should think about capacity. We should store ~256 keycode events @@ -32,40 +35,33 @@ public final class Input: Resource, @unchecked Sendable { private(set) var gamepads: [Int: Gamepad] = [:] var cursorStates: [CursorShape] = [.arrow] + public var rumbleGameControllerEngine: RumbleGameControllerEngine? + init() {} // MARK: - Public Methods /// Returns set of touches on screens. - public static func getTouches() -> Set { - lock.lock() - defer { lock.unlock() } - - return self.shared.touches + public func getTouches() -> Set { + return self.touches } /// Returns a set of input events. - public static func getInputEvents() -> Array { - lock.lock() - defer { lock.unlock() } - - return self.shared.eventsPool + public func getInputEvents() -> Array { + return self.eventsPool } /// Returns `true` if you are pressing the Latin key in the current keyboard layout. - public static func isKeyPressed(_ keyCode: KeyCode) -> Bool { - lock.lock() - defer { lock.unlock() } - - return self.shared.keyEvents.contains(keyCode) + public func isKeyPressed(_ keyCode: KeyCode) -> Bool { + return self.keyEvents.contains(keyCode) } /// Returns true if you are pressing the mouse button specified with MouseButton. - public static func isMouseButtonPressed(_ button: MouseButton) -> Bool { + public func isMouseButtonPressed(_ button: MouseButton) -> Bool { lock.lock() defer { lock.unlock() } - guard let phase = self.shared.mouseEvents[button]?.phase else { + guard let phase = self.mouseEvents[button]?.phase else { return false } @@ -73,56 +69,56 @@ public final class Input: Resource, @unchecked Sendable { } /// Returns `true` if you are released the mouse button. - public static func isMouseButtonRelease(_ button: MouseButton) -> Bool { + public func isMouseButtonRelease(_ button: MouseButton) -> Bool { lock.lock() defer { lock.unlock() } - return self.shared.mouseEvents[button]?.phase == .ended + return self.mouseEvents[button]?.phase == .ended } /// Get mouse position on window. - public static func getMousePosition() -> Vector2 { + public func getMousePosition() -> Vector2 { lock.lock() defer { lock.unlock() } - return self.shared.mousePosition + return self.mousePosition } /// Get mouse mode for active window. @MainActor - public static func getMouseMode() -> MouseMode { + public func getMouseMode() -> MouseMode { // Application.shared.windowManager.getMouseMode() return .visible } /// Set mouse mode for active window. @MainActor - public static func setMouseMode(_ mode: MouseMode) { + public mutating func setMouseMode(_ mode: MouseMode) { // Application.shared.windowManager.setMouseMode(mode) } /// Set current cursor shape. @MainActor - public static func setCursorShape(_ shape: CursorShape) { - self.shared.cursorStates = [shape] + public mutating func setCursorShape(_ shape: CursorShape) { + self.cursorStates = [shape] // Application.shared.windowManager.setCursorShape(shape) } /// Pushes a new cursor shape onto the stack and sets it as the current cursor shape. @MainActor - public static func pushCursorShape(_ shape: CursorShape) { - self.shared.cursorStates.append(shape) + public mutating func pushCursorShape(_ shape: CursorShape) { + self.cursorStates.append(shape) // Application.shared.windowManager.setCursorShape(shape) } /// Pops the last cursor shape from the stack and sets it as the current cursor shape. @MainActor - public static func popCursorShape() { - if self.shared.cursorStates.count > 2 { - self.shared.cursorStates.removeLast() + public mutating func popCursorShape() { + if self.cursorStates.count > 2 { + self.cursorStates.removeLast() } - let shape = self.shared.cursorStates.last! + let shape = self.cursorStates.last! // Application.shared.windowManager.setCursorShape(shape) } @@ -144,12 +140,12 @@ public final class Input: Resource, @unchecked Sendable { // MARK: Internal @MainActor - @_spi(Internal) public func removeEvents() { + @_spi(Internal) public mutating func removeEvents() { self.eventsPool.removeAll() } @MainActor - @_spi(Internal) public func receiveEvent(_ event: T) { + @_spi(Internal) public mutating func receiveEvent(_ event: T) { self.eventsPool.append(event) self.parseInputEvent(event) } @@ -157,7 +153,7 @@ public final class Input: Resource, @unchecked Sendable { // MARK: - Private @MainActor - private func parseInputEvent(_ event: T) { + private mutating func parseInputEvent(_ event: T) { switch event { case let keyEvent as KeyEvent: if keyEvent.keyCode == .none && keyEvent.isRepeated { @@ -180,7 +176,8 @@ public final class Input: Resource, @unchecked Sendable { self.gamepads[gamepadConnectionEvent.gamepadId] = Gamepad( gamepadId: gamepadConnectionEvent.gamepadId, - info: gamepadConnectionEvent.gamepadInfo + info: gamepadConnectionEvent.gamepadInfo, + rumbleGameControllerEngine: self.rumbleGameControllerEngine ) print("Gamepad connected: ID \(gamepadConnectionEvent.gamepadId), Name: \(controllerName), Type: \(controllerType)") @@ -217,10 +214,8 @@ public final class Input: Resource, @unchecked Sendable { /// /// Gamepad IDs are typically assigned by the system. /// - Returns: An array of ``Input.Gamepad`` values representing the IDs of connected gamepads. - public static func getConnectedGamepads() -> [Gamepad] { - lock.lock() - defer { lock.unlock() } - return Array(self.shared.gamepads.values) + public func getConnectedGamepads() -> [Gamepad] { + return Array(self.gamepads.values) } /// Retrieves a gamepad by its ID. @@ -228,10 +223,8 @@ public final class Input: Resource, @unchecked Sendable { /// Gamepad IDs are typically assigned by the system. /// - Parameter gamepadId: The unique identifier of the gamepad. /// - Returns: A ``Gamepad`` value representing the gamepad, or `nil` if the gamepad is not connected. - public static func getConnectedGamepad(for gamepadId: Gamepad.ID) -> Gamepad? { - lock.lock() - defer { lock.unlock() } - return self.shared.gamepads[gamepadId] + public func getConnectedGamepad(for gamepadId: Gamepad.ID) -> Gamepad? { + return self.gamepads[gamepadId] } } @@ -262,7 +255,7 @@ public extension Input { } /// Available list of cursor shapes. - enum CursorShape { + enum CursorShape: Sendable { /// Standard cursor. case arrow @@ -319,7 +312,7 @@ public extension Input { extension Input { /// For test - func _removeAllStates() { + mutating func _removeAllStates() { self.gamepads.removeAll() self.cursorStates.removeAll() self.eventsPool.removeAll() @@ -344,7 +337,7 @@ public struct GamepadInfo: Hashable, Sendable { } /// Represents a connected gamepad. -public struct Gamepad { +public struct Gamepad: Sendable { /// The type alias for the gamepad ID. public typealias ID = Int @@ -365,7 +358,13 @@ public struct Gamepad { /// - Returns: A `GamepadInfo` struct containing details about the gamepad, or `nil` if the gamepad is not connected. public internal(set) var info: GamepadInfo? // TODO: Populate this later - init(gamepadId: ID, info: GamepadInfo? = nil) { + private var rumbleGameControllerEngine: RumbleGameControllerEngine? + + init( + gamepadId: ID, + info: GamepadInfo? = nil, + rumbleGameControllerEngine: RumbleGameControllerEngine? + ) { self.gamepadId = gamepadId self.info = info // Initialize all axes to 0.0 @@ -403,21 +402,25 @@ public struct Gamepad { /// - lowFrequency: The intensity of the low-frequency motor (typically 0.0 to 1.0). /// - highFrequency: The intensity of the high-frequency motor (typically 0.0 to 1.0). /// - duration: The duration of the rumble effect in seconds. - @MainActor public func rumble( lowFrequency: Float, highFrequency: Float, duration: Float ) { -#if canImport(Darwin) - AppleGameControllerManager.shared.rumbleGamepad( + rumbleGameControllerEngine?.rumbleGamepad( gamepadId: gamepadId, lowFrequency: lowFrequency, highFrequency: highFrequency, duration: duration ) -#else - print("Gamepad rumble not supported on this platform or for this gamepad ID \(gamepadId).") -#endif } } + +public protocol RumbleGameControllerEngine: AnyObject, Sendable { + func rumbleGamepad( + gamepadId: Int, + lowFrequency: Float, + highFrequency: Float, + duration: Float + ) +} diff --git a/Sources/AdaInput/InputPlugin.swift b/Sources/AdaInput/InputPlugin.swift index 3f1abafb1..9cc1367bc 100644 --- a/Sources/AdaInput/InputPlugin.swift +++ b/Sources/AdaInput/InputPlugin.swift @@ -5,12 +5,22 @@ // Created by Vladislav Prusakov on 29.05.2025. // +import AdaECS import AdaApp public struct InputPlugin: Plugin { public init() { } public func setup(in app: AppWorlds) { - app.insertResource(Input.shared) + app.insertResource(Input()) + app.addSystem(InputPostUpdateSystem.self, on: .postUpdate) } } + +@System +@inline(__always) +@MainActor func InputPostUpdate( + _ input: ResMut +) async { + input.wrappedValue.removeEvents() +} diff --git a/Sources/AdaInput/Keyboard.swift b/Sources/AdaInput/Keyboard.swift index 398f17dc6..61a7b9f0e 100644 --- a/Sources/AdaInput/Keyboard.swift +++ b/Sources/AdaInput/Keyboard.swift @@ -6,7 +6,11 @@ // import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif open class Keyboard { diff --git a/Sources/AdaPhysics/Physics2D/Body2D.swift b/Sources/AdaPhysics/Physics2D/Body2D.swift index 257d26e88..9ca4b58f1 100644 --- a/Sources/AdaPhysics/Physics2D/Body2D.swift +++ b/Sources/AdaPhysics/Physics2D/Body2D.swift @@ -12,13 +12,13 @@ import Math /// This class is used to hold a box2d body reference. public final class Body2D: @unchecked Sendable { - + weak var world: PhysicsWorld2D? weak var entity: Entity? let bodyId: b2BodyId - internal init(world: PhysicsWorld2D, bodyId: b2BodyId, entity: Entity) { + internal init(world: consuming PhysicsWorld2D, bodyId: b2BodyId, entity: consuming Entity) { self.world = world self.bodyId = bodyId self.entity = entity @@ -34,7 +34,7 @@ public final class Body2D: @unchecked Sendable { transform: Transform, shapeDef: b2ShapeDef ) -> BoxShape2D { - let shapeId = BoxShape2D.makeShape( + let shapeId = unsafe BoxShape2D.makeShape( for: shapeResource, transform: transform, shapeDef: shapeDef, @@ -49,8 +49,8 @@ public final class Body2D: @unchecked Sendable { func getShapes() -> [BoxShape2D] { let shapes = UnsafeMutablePointer.allocate(capacity: Int(shapesCount)) - b2Body_GetShapes(bodyId, shapes, shapesCount) - return Array(UnsafeBufferPointer(start: shapes, count: Int(shapesCount))).map { + unsafe b2Body_GetShapes(bodyId, shapes, shapesCount) + return unsafe Array(UnsafeBufferPointer(start: shapes, count: Int(shapesCount))).map { BoxShape2D(shape: $0) } } @@ -145,7 +145,7 @@ final class BoxShape2D { private let shape: b2ShapeId - init(shape: b2ShapeId) { + init(shape: consuming b2ShapeId) { self.shape = shape } @@ -169,7 +169,7 @@ final class BoxShape2D { return nil } - return Unmanaged.fromOpaque(ptr).takeUnretainedValue() + return unsafe Unmanaged.fromOpaque(ptr).takeUnretainedValue() } var filter: b2Filter { @@ -202,21 +202,21 @@ final class BoxShape2D { ) -> b2ShapeId { switch shape.fixture { case .polygon(let shape): - var hull = shape.verticies.withUnsafeBytes { ptr in - let baseAddress = ptr.assumingMemoryBound(to: b2Vec2.self).baseAddress - return b2ComputeHull(baseAddress, Int32(shape.verticies.count)) + var hull = unsafe shape.verticies.withUnsafeBytes { ptr in + let baseAddress = unsafe ptr.assumingMemoryBound(to: b2Vec2.self).baseAddress + return unsafe b2ComputeHull(baseAddress, Int32(shape.verticies.count)) } - let polygon = b2MakeOffsetPolygon(&hull, shape.offset.b2Vec, b2Rot_identity) - return withUnsafePointer(to: shapeDef) { shapeDefPtr in - withUnsafePointer(to: polygon) { polygonPtr in - b2CreatePolygonShape(bodyId, shapeDefPtr, polygonPtr) + let polygon = unsafe b2MakeOffsetPolygon(&hull, shape.offset.b2Vec, b2Rot_identity) + return unsafe withUnsafePointer(to: shapeDef) { shapeDefPtr in + unsafe withUnsafePointer(to: polygon) { polygonPtr in + unsafe b2CreatePolygonShape(bodyId, shapeDefPtr, polygonPtr) } } case .circle(let shape): var circle = b2Circle(center: Vector2.zero.b2Vec, radius: shape.radius * transform.scale.x) - return withUnsafePointer(to: shapeDef) { shapeDefPtr in - b2CreateCircleShape(bodyId, shapeDefPtr, &circle) + return unsafe withUnsafePointer(to: shapeDef) { shapeDefPtr in + unsafe b2CreateCircleShape(bodyId, shapeDefPtr, &circle) } case .box(let shape): let polygon = b2MakeBox( @@ -224,9 +224,9 @@ final class BoxShape2D { transform.scale.y * shape.halfHeight ) - return withUnsafePointer(to: shapeDef) { shapeDefPtr in - withUnsafePointer(to: polygon) { polygonPtr in - b2CreatePolygonShape(bodyId, shapeDefPtr, polygonPtr) + return unsafe withUnsafePointer(to: shapeDef) { shapeDefPtr in + unsafe withUnsafePointer(to: polygon) { polygonPtr in + unsafe b2CreatePolygonShape(bodyId, shapeDefPtr, polygonPtr) } } } diff --git a/Sources/AdaPhysics/Physics2D/Components/PhysicsJoint2DComponent.swift b/Sources/AdaPhysics/Physics2D/Components/PhysicsJoint2DComponent.swift index 4c8566328..b4865be3d 100644 --- a/Sources/AdaPhysics/Physics2D/Components/PhysicsJoint2DComponent.swift +++ b/Sources/AdaPhysics/Physics2D/Components/PhysicsJoint2DComponent.swift @@ -33,6 +33,7 @@ public final class PhysicsJoint2DDescriptor: Codable, Sendable { } @Component +@safe public struct PhysicsJoint2DComponent: @unchecked Sendable { let jointDescriptor: PhysicsJoint2DDescriptor diff --git a/Sources/AdaPhysics/Physics2D/DebugPhysics2DSystem.swift b/Sources/AdaPhysics/Physics2D/DebugPhysics2DSystem.swift index 6978e8916..1e16c8080 100644 --- a/Sources/AdaPhysics/Physics2D/DebugPhysics2DSystem.swift +++ b/Sources/AdaPhysics/Physics2D/DebugPhysics2DSystem.swift @@ -34,85 +34,84 @@ struct ExctractedPhysicsMesh2DDebug { } /// System for exctracting physics bodies for debug rendering. -@System +@PlainSystem public struct DebugPhysicsExctract2DSystem { @EntityQuery( where: (.has(PhysicsBody2DComponent.self) || .has(Collision2DComponent.self) || .has(PhysicsJoint2DComponent.self)) && .has(Visibility.self) ) private var entities - + @Query private var cameras - + public init(world: World) { } - public func update(context: inout UpdateContext) { + @MainActor + public func update(context: UpdateContext) { guard let (camera, globalTransform) = self.cameras.first else { return } let world = context.world - context.taskGroup.addTask { @MainActor in - guard world.getResource(PhysicsDebugOptions.self)?.contains(.showPhysicsShapes) == true else { - return - } - - guard let world = world.physicsWorld2D else { - return - } - -// guard let window = scene?.window else { -// return -// } -// -// var graphics = UIGraphicsContext(window: window) -// graphics.beginDraw(in: window.frame.size, scaleFactor: 1) -// -// if let viewUniform = camera.components[GlobalViewUniform.self] { -// let viewMatrix = viewUniform.viewProjectionMatrix -// graphics.concatenate(viewMatrix) -// graphics.scaleBy(x: window.frame.size.width / 2, y: window.frame.size.height / 2) -// graphics.translateBy(x: window.frame.size.width / 2, y: -window.frame.size.height / 2) -// } -// -// let drawContext = WorldDebugDrawContext() -// var debugDraw = b2DefaultDebugDraw() -// debugDraw.DrawSolidPolygon = DebugPhysicsExctract2DSystem_DrawSolidPolygon -// debugDraw.DrawSolidCircle = DebugPhysicsExctract2DSystem_DrawSolidCircle -// debugDraw.context = Unmanaged.passUnretained(drawContext).toOpaque() -// debugDraw.drawShapes = true -// debugDraw.drawAABBs = true -// world.debugDraw(with: debugDraw) -// -// drawContext.forEach { item in -// switch item { -// case let .line(start, end, color): -// graphics.drawLine( -// start: start, -// end: end, -// lineWidth: 2.0, -// color: color -// ) -// case let .circle(center, radius, color): -// graphics.drawEllipse( -// in: Rect( -// x: center.x - radius, -// y: -center.y - radius, -// width: radius * 2, -// height: radius * 2 -// ), -// color: color, -// thickness: 0.1 -// ) -// } -// } -// -// graphics.commitDraw() + guard world.getResource(PhysicsDebugOptions.self)?.contains(.showPhysicsShapes) == true else { + return + } + + guard let world = world.physicsWorld2D else { + return } + + // guard let window = scene?.window else { + // return + // } + // + // var graphics = UIGraphicsContext(window: window) + // graphics.beginDraw(in: window.frame.size, scaleFactor: 1) + // + // if let viewUniform = camera.components[GlobalViewUniform.self] { + // let viewMatrix = viewUniform.viewProjectionMatrix + // graphics.concatenate(viewMatrix) + // graphics.scaleBy(x: window.frame.size.width / 2, y: window.frame.size.height / 2) + // graphics.translateBy(x: window.frame.size.width / 2, y: -window.frame.size.height / 2) + // } + // + // let drawContext = WorldDebugDrawContext() + // var debugDraw = b2DefaultDebugDraw() + // debugDraw.DrawSolidPolygon = DebugPhysicsExctract2DSystem_DrawSolidPolygon + // debugDraw.DrawSolidCircle = DebugPhysicsExctract2DSystem_DrawSolidCircle + // debugDraw.context = Unmanaged.passUnretained(drawContext).toOpaque() + // debugDraw.drawShapes = true + // debugDraw.drawAABBs = true + // world.debugDraw(with: debugDraw) + // + // drawContext.forEach { item in + // switch item { + // case let .line(start, end, color): + // graphics.drawLine( + // start: start, + // end: end, + // lineWidth: 2.0, + // color: color + // ) + // case let .circle(center, radius, color): + // graphics.drawEllipse( + // in: Rect( + // x: center.x - radius, + // y: -center.y - radius, + // width: radius * 2, + // height: radius * 2 + // ), + // color: color, + // thickness: 0.1 + // ) + // } + // } + // + // graphics.commitDraw() } - @MainActor + @MainActor private func getRuntimeBody(from entity: Entity) -> Body2D? { return entity.components[PhysicsBody2DComponent.self]?.runtimeBody ?? entity.components[Collision2DComponent.self]?.runtimeBody @@ -129,51 +128,51 @@ public struct DebugPhysicsExctract2DSystem { //void ( *DrawString )( b2Vec2 p, const char* s, b2HexColor color, void* context ); private final class WorldDebugDrawContext { - + enum DebugItem { case line(start: Vector2, end: Vector2, color: Color) case circle(center: Vector2, radius: Float, color: Color) } - + private var drawStack: [DebugItem] = [] - + func addLine(start: Vector2, end: Vector2, color: Color) { self.drawStack.append(.line(start: start, end: end, color: color)) } - + func addCircle(center: Vector2, radius: Float, color: Color) { self.drawStack.append(.circle(center: center, radius: radius, color: color)) } - + func forEach(_ block: (DebugItem) -> Void) { self.drawStack.forEach(block) } } private func DebugPhysicsExctract2DSystem_DrawSolidCircle( - _ transform: b2Transform, + _ transform: b2Transform, _ radius: Float, _ color: b2HexColor, _ context: UnsafeMutableRawPointer? ) { - let debugContext = Unmanaged + let debugContext = unsafe Unmanaged .fromOpaque(context!) .takeUnretainedValue() - + let color = Color.fromHex(Int(color.rawValue)) - + let center = Vector2(transform.p.x, transform.p.y) - + debugContext.addCircle(center: center, radius: radius, color: color) - + let direction = Vector2( transform.q.c * radius, // cos(angle) * radius transform.q.s * radius // sin(angle) * radius ) - + let start = center let end = center + direction - + debugContext.addLine(start: start, end: end, color: color) } @@ -185,101 +184,99 @@ private func DebugPhysicsExctract2DSystem_DrawSolidPolygon( _ color: b2HexColor, _ context: UnsafeMutableRawPointer? ) { - guard let verticies else { + guard let verticies = unsafe verticies else { return } - - let debugContext = Unmanaged + + let debugContext = unsafe Unmanaged .fromOpaque(context!) .takeUnretainedValue() let color = Color.fromHex(Int(color.rawValue)) - + let vertices = (0..>> private var cameras - + @Query private var meshes -// @ResQuery -// private var meshDrawPass: Mesh2DDrawPass? + // @Res + // private var meshDrawPass: Mesh2DDrawPass? public init(world: World) {} - - public func update(context: inout UpdateContext) { - -// self.cameras.forEach { visibleEntities, renderItems in -// self.draw( -// visibleEntities: visibleEntities, -// items: &renderItems.wrappedValue.items -// ) -// } -// } -// -// private func draw( -// visibleEntities: VisibleEntities, -// items: inout [Transparent2DRenderItem] -// ) { -// itemIterator: -// for item in self.meshes { -// if !visibleEntities.entityIds.contains(item.entityId) { -// continue -// } -// -// let uniform = Mesh2DUniform( -// model: item.transform, -// modelInverseTranspose: .identity -// ) -// -// for model in item.mesh.models { -// for part in model.parts { -// guard let pipeline = item.material.getOrCreatePipeline(for: part.vertexDescriptor, keys: []) else { -// assertionFailure("Failed to create pipeline") -// continue itemIterator -// } -// -// let emptyEntity = EmptyEntity() -// emptyEntity.components += ExctractedMeshPart2d(part: part, material: item.material, modelUniform: uniform) -// -// items.append( -// Transparent2DRenderItem( -// entity: emptyEntity, -// batchEntity: emptyEntity, -// drawPassId: Self.mesh2dDrawPassIdentifier, -// renderPipeline: pipeline, -// sortKey: .greatestFiniteMagnitude // by default we render debug entities on top of scene. -// ) -// ) -// } -// } -// } + + public func update(context: UpdateContext) { + + // self.cameras.forEach { visibleEntities, renderItems in + // self.draw( + // visibleEntities: visibleEntities, + // items: &renderItems.wrappedValue.items + // ) + // } + // } + // + // private func draw( + // visibleEntities: VisibleEntities, + // items: inout [Transparent2DRenderItem] + // ) { + // itemIterator: + // for item in self.meshes { + // if !visibleEntities.entityIds.contains(item.entityId) { + // continue + // } + // + // let uniform = Mesh2DUniform( + // model: item.transform, + // modelInverseTranspose: .identity + // ) + // + // for model in item.mesh.models { + // for part in model.parts { + // guard let pipeline = item.material.getOrCreatePipeline(for: part.vertexDescriptor, keys: []) else { + // assertionFailure("Failed to create pipeline") + // continue itemIterator + // } + // + // let emptyEntity = EmptyEntity() + // emptyEntity.components += ExctractedMeshPart2d(part: part, material: item.material, modelUniform: uniform) + // + // items.append( + // Transparent2DRenderItem( + // entity: emptyEntity, + // batchEntity: emptyEntity, + // drawPassId: Self.mesh2dDrawPassIdentifier, + // renderPipeline: pipeline, + // sortKey: .greatestFiniteMagnitude // by default we render debug entities on top of scene. + // ) + // ) + // } + // } + // } } } diff --git a/Sources/AdaPhysics/Physics2D/Physics2DPlugin.swift b/Sources/AdaPhysics/Physics2D/Physics2DPlugin.swift index b71ae81c3..ab6145be4 100644 --- a/Sources/AdaPhysics/Physics2D/Physics2DPlugin.swift +++ b/Sources/AdaPhysics/Physics2D/Physics2DPlugin.swift @@ -32,8 +32,8 @@ public struct Physics2DPlugin: Plugin { return } renderWorld - .addSystem(Physics2DDebugDrawSystem.self) - .addSystem(DebugPhysicsExctract2DSystem.self) + .addSystem(Physics2DDebugDrawSystem.self, on: .render) + .addSystem(DebugPhysicsExctract2DSystem.self, on: .extract) } } diff --git a/Sources/AdaPhysics/Physics2D/Physics2DSystem.swift b/Sources/AdaPhysics/Physics2D/Physics2DSystem.swift index f75e2297f..959929c8a 100644 --- a/Sources/AdaPhysics/Physics2D/Physics2DSystem.swift +++ b/Sources/AdaPhysics/Physics2D/Physics2DSystem.swift @@ -14,43 +14,42 @@ import Math // - TODO: (Vlad) Runtime update shape resource /// A system for simulate and update physics bodies on the scene. -@System +@PlainSystem public struct Physics2DSystem: Sendable { public init(world: World) { } - @Query, Ref>(filter: [.stored, .added]) + @Query, Ref> private var physicsBodyQuery - @Query, Ref>(filter: [.stored, .added]) + @Query, Ref> private var collisionQuery @Query, Ref> private var jointsQuery - @ResQuery + @Res private var physicsWorld - public func update(context: inout UpdateContext) { - let deltaTime = context.deltaTime - context.taskGroup.addTask { @MainActor in - guard let world = self.physicsWorld?.world else { - return - } + @Res + private var fixedTime - world.updateSimulation(deltaTime) - world.processContacts() - world.processSensors() - self.updatePhysicsBodyEntities(in: world) - self.updateCollisionEntities(in: world) - } + @MainActor + public func update(context: UpdateContext) { + let deltaTime = fixedTime.deltaTime + let world = self.physicsWorld.world + world.updateSimulation(deltaTime) + world.processContacts() + world.processSensors() + self.updatePhysicsBodyEntities(in: world) + self.updateCollisionEntities(in: world) } // MARK: - Private @MainActor private func updatePhysicsBodyEntities(in world: PhysicsWorld2D) { - for (entity, physicsBody, transform) in self.physicsBodyQuery { + self.physicsBodyQuery.forEach { (entity, physicsBody, transform) in if let body = physicsBody.runtimeBody { if physicsBody.mode == .static { body.setTransform( @@ -67,29 +66,29 @@ public struct Physics2DSystem: Sendable { body.massData.mass = physicsBody.massProperties.mass } else { var def = b2DefaultBodyDef() - def.fixedRotation = physicsBody.fixedRotation - def.position = transform.position.xy.b2Vec - def.type = physicsBody.mode.b2Type + unsafe def.fixedRotation = physicsBody.fixedRotation + unsafe def.position = transform.position.xy.b2Vec + unsafe def.type = physicsBody.mode.b2Type - let body = world.createBody(with: def, for: entity) + let body = unsafe world.createBody(with: def, for: entity) physicsBody.runtimeBody = body for shapeResource in physicsBody.wrappedValue.shapes { var shapeDef = b2DefaultShapeDef() - shapeDef.density = physicsBody.material.density - shapeDef.restitution = physicsBody.material.restitution - shapeDef.friction = physicsBody.material.friction - shapeDef.filter = physicsBody.filter.b2Filter - + unsafe shapeDef.density = physicsBody.material.density + unsafe shapeDef.restitution = physicsBody.material.restitution + unsafe shapeDef.friction = physicsBody.material.friction + unsafe shapeDef.filter = physicsBody.filter.b2Filter + if physicsBody.wrappedValue.isTrigger { - shapeDef.isSensor = true + unsafe shapeDef.isSensor = true } if let debugColor = physicsBody.debugColor { - shapeDef.customColor = UInt32(debugColor.toHex) + unsafe shapeDef.customColor = UInt32(debugColor.toHex) } - body.appendShape( + unsafe body.appendShape( shapeResource, transform: transform.wrappedValue, shapeDef: shapeDef @@ -116,7 +115,7 @@ public struct Physics2DSystem: Sendable { @MainActor private func updateCollisionEntities(in world: PhysicsWorld2D) { - for (entity, collisionBody, transform) in collisionQuery { + collisionQuery.forEach { (entity, collisionBody, transform) in if let body = collisionBody.runtimeBody { if body.getPosition() != transform.position.xy { body.setTransform( @@ -126,23 +125,23 @@ public struct Physics2DSystem: Sendable { } } else { var def = b2DefaultBodyDef() - def.position = transform.position.xy.b2Vec - def.type = b2_staticBody + unsafe def.position = transform.position.xy.b2Vec + unsafe def.type = b2_staticBody - let body = world.createBody(with: def, for: entity) + let body = unsafe world.createBody(with: def, for: entity) collisionBody.runtimeBody = body for shapeResource in collisionBody.wrappedValue.shapes { var shapeDef = b2DefaultShapeDef() - shapeDef.density = 1 + unsafe shapeDef.density = 1 if let debugColor = collisionBody.debugColor { - shapeDef.customColor = UInt32(debugColor.toHex) + unsafe shapeDef.customColor = UInt32(debugColor.toHex) } - shapeDef.filter = collisionBody.filter.b2Filter + unsafe shapeDef.filter = collisionBody.filter.b2Filter if case .trigger = collisionBody.mode { - shapeDef.isSensor = true + unsafe shapeDef.isSensor = true } - body.appendShape( + unsafe body.appendShape( shapeResource, transform: transform.wrappedValue, shapeDef: shapeDef diff --git a/Sources/AdaPhysics/Physics2D/PhysicsWorld2D.swift b/Sources/AdaPhysics/Physics2D/PhysicsWorld2D.swift index 46ff156e6..27b933853 100644 --- a/Sources/AdaPhysics/Physics2D/PhysicsWorld2D.swift +++ b/Sources/AdaPhysics/Physics2D/PhysicsWorld2D.swift @@ -11,8 +11,8 @@ import box2d import Math /// A protocol that defines a delegate for the physics world. -public protocol PhysicsWorld2DDelegate: AnyObject { - +public protocol PhysicsWorld2DDelegate: AnyObject, Sendable { + /// Called when the physics world is about to solve a collision. /// /// - Parameters: @@ -43,8 +43,7 @@ public protocol PhysicsWorld2DDelegate: AnyObject { } /// An object that holds and simulates all 2D physics bodies. -@MainActor -public final class PhysicsWorld2D: @preconcurrency Codable { +public final class PhysicsWorld2D: Codable, @unchecked Sendable { /// The coding keys for the physics world. enum CodingKeys: CodingKey { @@ -102,21 +101,42 @@ public final class PhysicsWorld2D: @preconcurrency Codable { } } + /// Adjust the restitution threshold. It is recommended not to make this value very small + /// because it will prevent bodies from sleeping. Usually in meters per second. + public var restitutionThreshold: Float { + get { + b2World_GetRestitutionThreshold(worldId) + } + set { + b2World_SetRestitutionThreshold(worldId, newValue) + } + } + + /// Adjust the hit event threshold. This controls the collision speed needed to generate a b2ContactHitEvent. + /// Usually in meters per second. + public var hitEventThreshold: Float { + get { + b2World_GetHitEventThreshold(worldId) + } + set { + b2World_SetHitEventThreshold(worldId, newValue) + } + } private let worldId: b2WorldId var eventManager: EventManager = .default /// - Parameter gravity: default gravity is 9.8. nonisolated init(gravity: Vector2 = [0, -9.81]) { var worldDef = b2DefaultWorldDef() - worldDef.gravity = gravity.b2Vec - worldDef.enableSleep = true - worldDef.enableContinuous = true - self.worldId = b2CreateWorld(&worldDef) + unsafe worldDef.gravity = gravity.b2Vec + unsafe worldDef.enableSleep = true + unsafe worldDef.enableContinuous = true + self.worldId = unsafe b2CreateWorld(&worldDef) b2World_EnableWarmStarting(worldId, true) - let unsafeWorldPtr = Unmanaged.passUnretained(self).toOpaque() - b2World_SetPreSolveCallback(worldId, PhysicsWorld2D_PreSolve, unsafeWorldPtr) - b2World_SetCustomFilterCallback(worldId, PhysicsWorld2D_CustomFilterCallback, unsafeWorldPtr) + let unsafeWorldPtr = unsafe Unmanaged.passUnretained(self).toOpaque() + unsafe b2World_SetPreSolveCallback(worldId, PhysicsWorld2D_PreSolve, unsafeWorldPtr) + unsafe b2World_SetCustomFilterCallback(worldId, PhysicsWorld2D_CustomFilterCallback, unsafeWorldPtr) } deinit { @@ -201,26 +221,26 @@ public final class PhysicsWorld2D: @preconcurrency Codable { @MainActor func debugDraw(with definitions: b2DebugDraw) { - var definitions = definitions - b2World_Draw(worldId, &definitions) + var definitions = unsafe definitions + unsafe b2World_Draw(worldId, &definitions) } @MainActor func processContacts() { let contactEvents = b2World_GetContactEvents(self.worldId) - for index in 0.. Body2D { - let body = withUnsafePointer(to: definition) { - b2CreateBody(self.worldId, $0) + let body = unsafe withUnsafePointer(to: definition) { + unsafe b2CreateBody(self.worldId, $0) } let body2d = Body2D(world: self, bodyId: body, entity: entity) - let pointer = Unmanaged.passUnretained(body2d).toOpaque() - b2Body_SetUserData(body, pointer) + let pointer = unsafe Unmanaged.passUnretained(body2d).toOpaque() + unsafe b2Body_SetUserData(body, pointer) return body2d } @@ -358,12 +378,12 @@ private func PhysicsWorld2D_PreSolve( _ manifold: UnsafeMutablePointer?, _ context: UnsafeMutableRawPointer? ) -> Bool { - guard let context else { + guard let context = unsafe context else { return false } - let world = Unmanaged.fromOpaque(context).takeUnretainedValue() - let manifold = manifold.flatMap { ptr in - Manifold2D( + let world = unsafe Unmanaged.fromOpaque(context).takeUnretainedValue() + let manifold = unsafe manifold.flatMap { ptr in + unsafe Manifold2D( normal: ptr.pointee.normal.asVector2, rollingImpulse: ptr.pointee.rollingImpulse ) @@ -398,10 +418,10 @@ private func PhysicsWorld2D_CustomFilterCallback( _ shapeB: b2ShapeId, _ context: UnsafeMutableRawPointer? ) -> Bool { - guard let context else { + guard let context = unsafe context else { return true } - let world = Unmanaged.fromOpaque(context).takeUnretainedValue() + let world = unsafe Unmanaged.fromOpaque(context).takeUnretainedValue() return MainActor.assumeIsolated { let shapeIdA = BoxShape2D(shape: shapeA) @@ -431,18 +451,18 @@ private func PhysicsWorld2D_CustomFilterCallback( extension Vector2 { var b2Vec: b2Vec2 { get { - return unsafeBitCast(self, to: b2Vec2.self) + return unsafe unsafeBitCast(self, to: b2Vec2.self) } set { - self = unsafeBitCast(newValue, to: Vector2.self) + self = unsafe unsafeBitCast(newValue, to: Vector2.self) } } } extension b2Vec2 { var asVector2: Vector2 { - return unsafeBitCast(self, to: Vector2.self) + return unsafe unsafeBitCast(self, to: Vector2.self) } } diff --git a/Sources/AdaPlatform/AppPlatformPlugin.swift b/Sources/AdaPlatform/AppPlatformPlugin.swift index b603d4d73..c9480ecf4 100644 --- a/Sources/AdaPlatform/AppPlatformPlugin.swift +++ b/Sources/AdaPlatform/AppPlatformPlugin.swift @@ -8,6 +8,7 @@ import AdaApp import AdaECS +import AdaUI public struct AppPlatformPlugin: Plugin { @@ -16,28 +17,31 @@ public struct AppPlatformPlugin: Plugin { @MainActor public func setup(in app: AppWorlds) { let argc = CommandLine.argc - let argv = CommandLine.unsafeArgv + let argv = unsafe CommandLine.unsafeArgv do { let application: Application #if os(macOS) - application = try MacApplication(argc: argc, argv: argv) + application = unsafe try MacApplication(argc: argc, argv: argv) #endif #if os(iOS) || os(tvOS) - application = try iOSApplication(argc: argc, argv: argv) + application = unsafe try iOSApplication(argc: argc, argv: argv) #endif #if os(Android) - application = try AndroidApplication(argc: argc, argv: argv) + application = unsafe try AndroidApplication(argc: argc, argv: argv) #endif #if os(Linux) - application = try LinuxApplication(argc: argc, argv: argv) + application = unsafe try LinuxApplication(argc: argc, argv: argv) #endif Application.shared = application - app.mainWorld.insertResource(application) + app.insertResource(application) + app.insertResource( + WindowManagerResource(windowManager: application.windowManager) + ) app.setRunner { worlds in do { diff --git a/Sources/AdaPlatform/Apple/MetalView.swift b/Sources/AdaPlatform/Apple/MetalView.swift index c8447ed89..d887f9651 100644 --- a/Sources/AdaPlatform/Apple/MetalView.swift +++ b/Sources/AdaPlatform/Apple/MetalView.swift @@ -13,7 +13,8 @@ import MetalKit open class MetalView: MTKView { public var windowID: UIWindow.ID - + weak var windowManager: UIWindowManager? + #if MACOS var currentTrackingArea: NSTrackingArea? #endif diff --git a/Sources/AdaPlatform/Apple/iOS/iOSApplication.swift b/Sources/AdaPlatform/Apple/iOS/iOSApplication.swift index 16deb850a..e680de0af 100644 --- a/Sources/AdaPlatform/Apple/iOS/iOSApplication.swift +++ b/Sources/AdaPlatform/Apple/iOS/iOSApplication.swift @@ -95,7 +95,7 @@ final class iOSApplication: Application { @objc private func update() { do { - try self.mainLoop.iterate() +// try self.mainLoop.iterate() } catch { print(error.localizedDescription) exit(-1) diff --git a/Sources/AdaPlatform/Apple/macOS/DisplayLink.swift b/Sources/AdaPlatform/Apple/macOS/DisplayLink.swift index 15aa1ac40..750ee89cd 100644 --- a/Sources/AdaPlatform/Apple/macOS/DisplayLink.swift +++ b/Sources/AdaPlatform/Apple/macOS/DisplayLink.swift @@ -22,17 +22,17 @@ public final class DisplayLink: @unchecked Sendable { var timerRef: CVDisplayLink? - var successLink = CVDisplayLinkCreateWithActiveCGDisplays(&timerRef) + var successLink = unsafe CVDisplayLinkCreateWithActiveCGDisplays(&timerRef) if let timer = timerRef { - successLink = CVDisplayLinkSetOutputCallback(timer, { _, _, _, _, _, source -> CVReturn in - if let source = source { - let sourceUnmanaged = Unmanaged.fromOpaque(source) - sourceUnmanaged.takeUnretainedValue().onEvent() + successLink = unsafe CVDisplayLinkSetOutputCallback(timer, { _, _, _, _, _, source -> CVReturn in + if let source = unsafe source { + let sourceUnmanaged = unsafe Unmanaged.fromOpaque(source) + unsafe sourceUnmanaged.takeUnretainedValue().onEvent() } return kCVReturnSuccess - }, Unmanaged.passUnretained(self.source).toOpaque()) + }, unsafe Unmanaged.passUnretained(self.source).toOpaque()) guard successLink == kCVReturnSuccess else { print("Failed to create timer with active display") diff --git a/Sources/AdaPlatform/Apple/macOS/MacApplication.swift b/Sources/AdaPlatform/Apple/macOS/MacApplication.swift index 62c756970..3af734a60 100644 --- a/Sources/AdaPlatform/Apple/macOS/MacApplication.swift +++ b/Sources/AdaPlatform/Apple/macOS/MacApplication.swift @@ -8,16 +8,18 @@ #if MACOS import AdaApp import AppKit -import AdaInput +@_spi(Internal) import AdaInput @_spi(Internal) import AdaUI import MetalKit +import AdaECS final class MacApplication: Application { private let delegate = MacAppDelegate() + private var gameControllerManager: AppleGameControllerManager? override init(argc: Int32, argv: UnsafeMutablePointer?>) throws { - try super.init(argc: argc, argv: argv) + unsafe try super.init(argc: argc, argv: argv) self.windowManager = MacOSWindowManager() UIWindowManager.setShared(self.windowManager) @@ -28,8 +30,6 @@ final class MacApplication: Application { app.finishLaunching() app.delegate = self.delegate - - AppleGameControllerManager.shared.startMonitoring() self.processEvents() app.activate(ignoringOtherApps: true) @@ -38,20 +38,21 @@ final class MacApplication: Application { private var task: Task? override func run(_ appWorlds: AppWorlds) throws { - task = Task { @MainActor in - self.mainLoop.setup() + setupInput(for: appWorlds) + task = Task(priority: .userInitiated) { do { while true { try Task.checkCancellation() self.processEvents() - try await self.mainLoop.iterate(appWorlds) + await appWorlds.update() + await Task.yield() } } catch { let alert = Alert( title: "AdaEngine finished with Error", message: error.localizedDescription, buttons: [ - .cancel("OK", action: { exit(EXIT_FAILURE)}) + .cancel("OK", action: { exit(EXIT_FAILURE) }) ] ) Application.shared.showAlert(alert) @@ -97,7 +98,20 @@ final class MacApplication: Application { // MARK: - Private - func processEvents() { + private func setupInput(for app: AppWorlds) { + self.gameControllerManager = AppleGameControllerManager { [unowned app] event in + Task { @MainActor in + let input = app.main.getRefResource(Input.self) + input.wrappedValue.receiveEvent(event) + } + } + let mutableInput = app.main.getRefResource(Input.self) + mutableInput.wrappedValue.rumbleGameControllerEngine = self.gameControllerManager + self.windowManager.inputRef = mutableInput + self.gameControllerManager?.startMonitoring() + } + + private func processEvents() { while true { let event = NSApp.nextEvent( matching: .any, diff --git a/Sources/AdaPlatform/Apple/macOS/MacOSScreenManager.swift b/Sources/AdaPlatform/Apple/macOS/MacOSScreenManager.swift index 2b21c2422..88a320814 100644 --- a/Sources/AdaPlatform/Apple/macOS/MacOSScreenManager.swift +++ b/Sources/AdaPlatform/Apple/macOS/MacOSScreenManager.swift @@ -40,12 +40,12 @@ class MacOSScreenManager: ScreenManager { var service: io_object_t = 1 var brighntess: Float = 1.0 - let dict: CFDictionary = IOServiceMatching("IODisplayConnect")! - let result = IOServiceGetMatchingServices(kIOMainPortDefault, dict, &iterator) + let dict: CFDictionary = unsafe IOServiceMatching("IODisplayConnect")! + let result = unsafe IOServiceGetMatchingServices(kIOMainPortDefault, dict, &iterator) if result == KERN_SUCCESS { while service != 0 { service = IOIteratorNext(iterator) - IODisplayGetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, &brighntess) + unsafe IODisplayGetFloatParameter(service, 0, kIODisplayBrightnessKey as CFString, &brighntess) IOObjectRelease(service) } } diff --git a/Sources/AdaPlatform/Apple/macOS/MacOSWindowManager.swift b/Sources/AdaPlatform/Apple/macOS/MacOSWindowManager.swift index 63df87603..961293521 100644 --- a/Sources/AdaPlatform/Apple/macOS/MacOSWindowManager.swift +++ b/Sources/AdaPlatform/Apple/macOS/MacOSWindowManager.swift @@ -11,6 +11,7 @@ import AdaRender import AppKit import AdaInput import Math +import AdaUtils // swiftlint:disable cyclomatic_complexity final class MacOSWindowManager: UIWindowManager { @@ -34,9 +35,9 @@ final class MacOSWindowManager: UIWindowManager { /// Register view in engine let metalView = MetalView(windowId: window.id, frame: contentRect) - + metalView.windowManager = self let sizeInt = SizeInt(width: Int(size.width), height: Int(size.height)) - try? RenderEngine.shared.createWindow(.windowId(window.id), for: metalView, size: sizeInt) + try? RenderEngine.shared.createWindow(window.id, for: metalView, size: sizeInt) let systemWindow = NSWindow( contentRect: contentRect, @@ -81,7 +82,6 @@ final class MacOSWindowManager: UIWindowManager { } window.windowDidAppear() - self.setActiveWindow(window) } @@ -127,7 +127,10 @@ final class MacOSWindowManager: UIWindowManager { } override func getScreen(for window: UIWindow) -> Screen? { - guard let nsWindow = window.systemWindow as? NSWindow, let screen = nsWindow.screen else { + guard + let nsWindow = window.systemWindow as? NSWindow, + let screen = nsWindow.screen + else { return nil } @@ -234,7 +237,7 @@ final class MacOSWindowManager: UIWindowManager { return } - guard let pixels = bitmap.bitmapData else { + guard let pixels = unsafe bitmap.bitmapData else { return } @@ -246,10 +249,10 @@ final class MacOSWindowManager: UIWindowManager { let color = image.getPixel(x: columnIndex, y: rowIndex) - pixels[index * 4 + 0] = UInt8(clamp(color.red * 255.0, 0, 255)) - pixels[index * 4 + 1] = UInt8(clamp(color.green * 255.0, 0, 255)) - pixels[index * 4 + 2] = UInt8(clamp(color.blue * 255.0, 0, 255)) - pixels[index * 4 + 3] = UInt8(clamp(color.alpha * 255.0, 0, 255)) + unsafe pixels[index * 4 + 0] = UInt8(clamp(color.red * 255.0, 0, 255)) + unsafe pixels[index * 4 + 1] = UInt8(clamp(color.green * 255.0, 0, 255)) + unsafe pixels[index * 4 + 2] = UInt8(clamp(color.blue * 255.0, 0, 255)) + unsafe pixels[index * 4 + 3] = UInt8(clamp(color.alpha * 255.0, 0, 255)) } let nsImage = NSImage(size: CGSize(width: CGFloat(texture.width), height: CGFloat(texture.height))) @@ -343,7 +346,7 @@ final class NSWindowDelegateObject: NSObject, NSWindowDelegate { } let sizeInt = SizeInt(width: Int(size.width), height: Int(size.height)) - try? RenderEngine.shared.resizeWindow(.windowId(window.id), newSize: sizeInt) + try? RenderEngine.shared.resizeWindow(window.id, newSize: sizeInt) } func windowDidExitFullScreen(_ notification: Notification) { diff --git a/Sources/AdaPlatform/Apple/macOS/MetalView+macOS.swift b/Sources/AdaPlatform/Apple/macOS/MetalView+macOS.swift index 59d7e9974..f9334b4a3 100644 --- a/Sources/AdaPlatform/Apple/macOS/MetalView+macOS.swift +++ b/Sources/AdaPlatform/Apple/macOS/MetalView+macOS.swift @@ -8,11 +8,17 @@ #if MACOS import AdaUtils @_spi(Internal) import AdaInput +@_spi(Internal) import AdaUI import AppKit import Math +import AdaECS extension MetalView { - + + var input: Ref? { + self.windowManager?.inputRef + } + public override var acceptsFirstResponder: Bool { return true } @@ -60,8 +66,8 @@ extension MetalView { time: TimeInterval(event.timestamp) ) - Input.shared.mousePosition = position - Input.shared.receiveEvent(mouseEvent) + input?.mousePosition = position + input?.wrappedValue.receiveEvent(mouseEvent) } open override func cursorUpdate(with event: NSEvent) { @@ -71,8 +77,8 @@ extension MetalView { public override func mouseDown(with event: NSEvent) { let position = self.mousePosition(for: event) - let isContinious = Input.shared.mouseEvents[.left]?.phase == .began - + let isContinious = input?.wrappedValue.mouseEvents[.left]?.phase == .began + let mouseEvent = MouseEvent( window: self.windowID, button: .left, @@ -82,13 +88,13 @@ extension MetalView { time: TimeInterval(event.timestamp) ) - Input.shared.mousePosition = position - Input.shared.receiveEvent(mouseEvent) + input?.mousePosition = position + input?.wrappedValue.receiveEvent(mouseEvent) } public override func mouseMoved(with event: NSEvent) { let position = self.mousePosition(for: event) - Input.shared.mousePosition = position + input?.mousePosition = position let event = MouseEvent( window: self.windowID, @@ -98,12 +104,12 @@ extension MetalView { modifierKeys: KeyModifier(modifiers: event.modifierFlags), time: TimeInterval(event.timestamp) ) - Input.shared.receiveEvent(event) + input?.wrappedValue.receiveEvent(event) } open override func mouseDragged(with event: NSEvent) { let position = self.mousePosition(for: event) - Input.shared.mousePosition = position + input?.mousePosition = position let event = MouseEvent( window: self.windowID, @@ -113,7 +119,7 @@ extension MetalView { modifierKeys: KeyModifier(modifiers: event.modifierFlags), time: TimeInterval(event.timestamp) ) - Input.shared.receiveEvent(event) + input?.wrappedValue.receiveEvent(event) } public override func scrollWheel(with event: NSEvent) { @@ -135,7 +141,7 @@ extension MetalView { time: TimeInterval(event.timestamp) ) - Input.shared.receiveEvent(mouseEvent) + input?.wrappedValue.receiveEvent(mouseEvent) } public override func keyUp(with event: NSEvent) { @@ -151,7 +157,7 @@ extension MetalView { isRepeated: event.isARepeat ) - Input.shared.receiveEvent(keyEvent) + input?.wrappedValue.receiveEvent(keyEvent) } public override func keyDown(with event: NSEvent) { @@ -167,7 +173,7 @@ extension MetalView { isRepeated: event.isARepeat ) - Input.shared.receiveEvent(keyEvent) + input?.wrappedValue.receiveEvent(keyEvent) } // MARK: - Private diff --git a/Sources/AdaPlatform/Application/Application.swift b/Sources/AdaPlatform/Application/Application.swift index c69cc9765..de80c23bd 100644 --- a/Sources/AdaPlatform/Application/Application.swift +++ b/Sources/AdaPlatform/Application/Application.swift @@ -8,7 +8,11 @@ import AdaApp import AdaECS @_spi(Internal) import AdaRender +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import AdaUI /// The main class represents application instance. @@ -20,8 +24,6 @@ open class Application: Resource { /// Contains application instance if application created from ``App``. @MainActor public internal(set) static var shared: Application! - let mainLoop: MainLoop = MainLoop.current - /// Current runtime platform. public var platform: RuntimePlatform { #if os(macOS) diff --git a/Sources/AdaPlatform/GameLoop.swift b/Sources/AdaPlatform/GameLoop.swift deleted file mode 100644 index 882257a2b..000000000 --- a/Sources/AdaPlatform/GameLoop.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// MainLoop.swift -// AdaEngine -// -// Created by v.prusakov on 11/2/21. -// - -import AdaApp -import AdaUtils -@_spi(AdaEngine) import AdaAssets -import AdaRender -@_spi(Internal) import AdaUI -@_spi(Internal) import AdaInput - -/// The main class responds to update all systems in engine. -/// You can have only one MainLoop per app. -@MainActor -public final class MainLoop { - - public private(set) static var current: MainLoop = MainLoop() - - private var lastUpdate: LongTimeInterval = 0 - - private(set) var isIterating = false - - private var isFirstTick: Bool = true - - private var fixedTimestep: FixedTimestep = FixedTimestep(step: 0) - - public func setup() { - let physicsTickPerSecond = Engine.shared.physicsTickPerSecond - self.fixedTimestep.step = 1 / AdaUtils.TimeInterval(physicsTickPerSecond) - } - - public func iterate(_ appWorlds: AppWorlds) async throws { - if self.isIterating { - return - } - - self.isIterating = true - defer { self.isIterating = false } - - let now = Time.absolute - let deltaTime = AdaUtils.TimeInterval(max(0, now - self.lastUpdate)) - self.lastUpdate = now - - // that little hack to avoid big delta in the first tick, because delta is equals Time.absolute value. - if self.isFirstTick { - self.isFirstTick = false - return - } - - EventManager.default.send(EngineEvents.MainLoopBegan(deltaTime: deltaTime)) - try await AssetsManager.processResources() - - try RenderEngine.shared.beginFrame() - await Application.shared.windowManager.update(deltaTime) - await appWorlds.update() - try RenderEngine.shared.endFrame() - Input.shared.removeEvents() - } -} diff --git a/Sources/AdaRender/Assets/Shaders/FullScreenShader.glsl b/Sources/AdaRender/Assets/Shaders/FullScreenShader.glsl new file mode 100644 index 000000000..e93904450 --- /dev/null +++ b/Sources/AdaRender/Assets/Shaders/FullScreenShader.glsl @@ -0,0 +1,23 @@ +#version 450 core +#pragma stage : vert + +layout (location = 0) out vec2 v_UV; + +void main() { + uint vertex_index = gl_VertexIndex; + vec2 uv = vec2(float(vertex_index >> 1u), float(vertex_index & 1u)) * 2.0; + gl_Position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); + v_UV = uv; +} + +#version 450 core +#pragma stage : frag + +layout (location = 0) in vec2 v_UV; +layout (location = 0) out vec4 o_Color; + +layout (binding = 0) uniform sampler2D u_MainTexture; + +void main() { + o_Color = texture(u_MainTexture, v_UV); +} diff --git a/Sources/AdaRender/Backends/CommandEncoder.swift b/Sources/AdaRender/Backends/CommandEncoder.swift new file mode 100644 index 000000000..15ff88f5e --- /dev/null +++ b/Sources/AdaRender/Backends/CommandEncoder.swift @@ -0,0 +1,157 @@ +// +// CommandEncoder.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 29.07.2025. +// + +import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif +import Math + +// MARK: - Common Descriptors + +public struct BlitPassDescriptor: Sendable { + public var label: String? + + public init(label: String? = nil) { + self.label = label + } +} + +// MARK: - Common Types + +public struct Origin3D: Sendable { + public var x: Int + public var y: Int + public var z: Int + + public init(x: Int = 0, y: Int = 0, z: Int = 0) { + self.x = x + self.y = y + self.z = z + } +} + +public struct Size3D: Sendable { + public var width: Int + public var height: Int + public var depth: Int + + public init(width: Int, height: Int, depth: Int = 1) { + self.width = width + self.height = height + self.depth = depth + } +} + +public protocol CommandBuffer: AnyObject { + func beginRenderPass(_ desc: RenderPassDescriptor) -> RenderCommandEncoder + + func beginBlitPass(_ desc: BlitPassDescriptor) -> BlitCommandEncoder + + func commit() +} + +public protocol BlitCommandEncoder: CommonCommandEncoder { + func copyTextureToTexture( + source: Texture, + sourceOrigin: Origin3D, + sourceSize: Size3D, + sourceMipLevel: Int, + sourceSlice: Int, + destination: Texture, + destinationOrigin: Origin3D, + destinationMipLevel: Int, + destinationSlice: Int + ) + + func copyBufferToBuffer( + source: Buffer, + sourceOffset: Int, + destination: Buffer, + destinationOffset: Int, + size: Int + ) + + func copyBufferToTexture( + source: Buffer, + sourceOffset: Int, + sourceBytesPerRow: Int, + sourceBytesPerImage: Int, + sourceSize: Size3D, + destination: Texture, + destinationOrigin: Origin3D, + destinationMipLevel: Int, + destinationSlice: Int + ) + + func copyTextureToBuffer( + source: Texture, + sourceOrigin: Origin3D, + sourceMipLevel: Int, + sourceSlice: Int, + sourceSize: Size3D, + destination: Buffer, + destinationOffset: Int, + destinationBytesPerRow: Int, + destinationBytesPerImage: Int + ) + + func generateMipmaps(for texture: Texture) + + func fillBuffer(_ buffer: Buffer, range: Range, value: UInt8) + + func endBlitPass() +} + +public protocol CommandQueue: AnyObject { + func makeCommandBuffer() -> CommandBuffer +} + +public protocol CommonCommandEncoder: AnyObject { + func pushDebugName(_ string: String) + + func popDebugName() +} + +public protocol RenderCommandEncoder: CommonCommandEncoder { + + func setRenderPipelineState(_ pipeline: RenderPipeline) + + func setVertexBuffer(_ buffer: UniformBuffer, offset: Int, index: Int) + + func setVertexBuffer(_ buffer: VertexBuffer, offset: Int, index: Int) + + func setFragmentBuffer(_ buffer: UniformBuffer, offset: Int, index: Int) + + func setVertexBuffer(_ bufferData: BufferData, offset: Int, index: Int) + + func setFragmentBuffer(_ bufferData: BufferData, offset: Int, index: Int) + + func setIndexBuffer(_ bufferData: BufferData, indexFormat: IndexBufferFormat) + + func setVertexBytes(_ bytes: UnsafeRawPointer, length: Int, index: Int) + + func setFragmentTexture(_ texture: Texture, index: Int) + + func setFragmentSamplerState(_ sampler: Sampler, index: Int) + + func setViewport(_ viewport: Rect) + + func setScissorRect(_ rect: Rect) + + func setTriangleFillMode(_ fillMode: TriangleFillMode) + + func setIndexBuffer(_ buffer: IndexBuffer, offset: Int) + + func drawIndexed(indexCount: Int, indexBufferOffset: Int, instanceCount: Int) + + func draw(type: IndexPrimitive, vertexStart: Int, vertexCount: Int, instanceCount: Int) + + func endRenderPass() +} diff --git a/Sources/AdaRender/Backends/Metal/MetalRenderBackend.swift b/Sources/AdaRender/Backends/Metal/MetalRenderBackend.swift index 9ba4504e0..86a92b2e7 100644 --- a/Sources/AdaRender/Backends/Metal/MetalRenderBackend.swift +++ b/Sources/AdaRender/Backends/Metal/MetalRenderBackend.swift @@ -8,28 +8,25 @@ // TODO: (Vlad) We should support bgra8Unorm_srgb (Should we?) #if METAL -@preconcurrency import Metal +@unsafe @preconcurrency import Metal import ModelIO import MetalKit import OrderedCollections import Math import AdaUtils -class MetalRenderBackend: RenderBackend { - +final class MetalRenderBackend: RenderBackend, @unchecked Sendable { + private let context: Context let type: RenderBackendType = .metal private(set) var currentFrameIndex: Int = 0 - private var inFlightSemaphore: DispatchSemaphore private var commandQueue: MTLCommandQueue private(set) var renderDevice: RenderDevice init(appName: String) { self.context = Context() - - self.inFlightSemaphore = DispatchSemaphore(value: RenderEngine.configurations.maxFramesInFlight) self.commandQueue = self.context.physicalDevice.makeCommandQueue()! self.renderDevice = MetalRenderDevice( @@ -46,12 +43,12 @@ class MetalRenderBackend: RenderBackend { ) } - func createWindow(_ windowId: WindowRef, for surface: RenderSurface, size: SizeInt) throws { + func createWindow(_ windowId: WindowID, for surface: RenderSurface, size: SizeInt) throws { let mtlView = (surface as! MTKView) try self.context.createRenderWindow(with: windowId, view: mtlView, size: size) } - func resizeWindow(_ windowId: WindowRef, newSize: SizeInt) throws { + func resizeWindow(_ windowId: WindowID, newSize: SizeInt) throws { guard newSize.width > 0 && newSize.height > 0 else { return } @@ -59,43 +56,35 @@ class MetalRenderBackend: RenderBackend { self.context.updateSizeForRenderWindow(windowId, size: newSize) } - func destroyWindow(_ window: WindowRef) throws { - guard case(.windowId(let windowId)) = window else { - return - } - - guard self.context.windows[windowId] != nil else { - return - } - + func destroyWindow(_ window: WindowID) throws { self.context.destroyWindow(by: window) } - - func beginFrame() throws { - self.inFlightSemaphore.wait() - for (_, window) in self.context.windows { - window.commandBuffer = self.commandQueue.makeCommandBuffer() -// window.drawable = window.view?.currentDrawable - window.drawable = (window.view?.layer as? CAMetalLayer)?.nextDrawable() + func getRenderWindow(for windowId: WindowID) -> RenderWindow? { + guard let window = self.context.windows[windowId] else { + return nil } + + return RenderWindow( + windowId: windowId, + height: window.size.height, + width: window.size.width, + scaleFactor: window.scaleFactor + ) } - - func endFrame() throws { - for window in self.context.windows.values { - guard let drawable = window.drawable, let commandBuffer = window.commandBuffer else { - return - } - - commandBuffer.addCompletedHandler { @Sendable [inFlightSemaphore] _ in - inFlightSemaphore.signal() - } - commandBuffer.present(drawable) - commandBuffer.commit() + func getRenderWindows() throws -> RenderWindows { + var windows = SparseSet() + for (id, window) in self.context.windows { + windows[id] = RenderWindow( + windowId: id, + height: window.size.height, + width: window.size.width, + scaleFactor: window.scaleFactor + ) } - - currentFrameIndex = (currentFrameIndex + 1) % RenderEngine.configurations.maxFramesInFlight + + return RenderWindows(windows: windows) } } @@ -308,15 +297,6 @@ extension SamplerMinMagFilter { } } -class MetalCommandBuffer: CommandBuffer { - - let commandBuffer: MTLCommandBuffer - - init(commandBuffer: MTLCommandBuffer) { - self.commandBuffer = commandBuffer - } -} - final class MetalRenderCommandBuffer: DrawCommandBuffer { let encoder: MTLRenderCommandEncoder let commandBuffer: MTLCommandBuffer @@ -329,11 +309,6 @@ final class MetalRenderCommandBuffer: DrawCommandBuffer { #endif -/// A protocol that defines a command buffer. -public protocol CommandBuffer { - -} - /// A protocol that defines a draw command buffer. public protocol DrawCommandBuffer: Sendable { diff --git a/Sources/AdaRender/Backends/Metal/MetalRenderContext.swift b/Sources/AdaRender/Backends/Metal/MetalRenderContext.swift index 8f5c7a357..83cba7606 100644 --- a/Sources/AdaRender/Backends/Metal/MetalRenderContext.swift +++ b/Sources/AdaRender/Backends/Metal/MetalRenderContext.swift @@ -14,9 +14,8 @@ import MetalKit extension MetalRenderBackend { - final class Context { - private(set) var windows: [RID: RenderWindow] = [:] - private var primaryWindow: RenderWindow? + final class Context: @unchecked Sendable { + private(set) var windows: [WindowID: MetalRenderWindow] = [:] let physicalDevice: MTLDevice init() { @@ -25,57 +24,52 @@ extension MetalRenderBackend { UserDefaults.standard.set(needsShowDebugHUD, forKey: "MetalForceHudEnabled") } - func getRenderWindow(for window: WindowRef) -> RenderWindow? { - switch window { - case .primary: - return primaryWindow - case .windowId(let id): - return windows[id] - } + func getRenderWindow(for window: WindowID) -> MetalRenderWindow? { + windows[window] } // MARK: - Methods - @MainActor func createRenderWindow(with id: WindowRef, view: MTKView, size: SizeInt) throws { - if case(.windowId(let id)) = id, self.windows[id] != nil { + @MainActor + func createRenderWindow(with id: WindowID, view: MTKView, size: SizeInt) throws { + if windows[id] != nil { throw ContextError.creationWindowAlreadyExists } - - let window = RenderWindow(view: view) - - // TODO: (Vlad) We should setup it in different place? + + #if canImport(AppKit) + var scaleFactor: Float = unsafe Float(view.window?.screen?.backingScaleFactor ?? 2) + #elseif canImport(UIKit) + var scaleFactor: Float = unsafe Float(view.window?.screen?.scaleFactor ?? 2) + #else + var scaleFactor: Float = 2 + #endif + + let window = MetalRenderWindow( + view: view, + size: size, + scaleFactor: scaleFactor + ) view.colorPixelFormat = .bgra8Unorm view.device = self.physicalDevice view.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1) view.framebufferOnly = false view.sampleCount = 1 - if self.primaryWindow == nil { - self.primaryWindow = window - } - if case .windowId(let id) = id { - self.windows[id] = window - } + let layer = view.layer as? CAMetalLayer + layer?.maximumDrawableCount = unsafe RenderEngine.configurations.maxFramesInFlight + layer?.allowsNextDrawableTimeout = true + + self.windows[id] = window } - func updateSizeForRenderWindow(_ windowId: WindowRef, size: SizeInt) { -// guard let window = self.windows[windowId] else { -// assertionFailure("Not found window by id \(windowId)") -// return -// } - -// window.view?.drawableSize = size.toCGSize + func updateSizeForRenderWindow(_ windowId: WindowID, size: SizeInt) { + windows[windowId]?.size = size } - func destroyWindow(by id: WindowRef) { - guard case .windowId(let id) = id else { - return - } - + func destroyWindow(by id: WindowID) { guard self.windows[id] != nil else { assertionFailure("Not found window by id \(id)") return } - self.windows[id] = nil } @@ -97,35 +91,23 @@ extension MetalRenderBackend { var errorDescription: String? { switch self { case .creationWindowAlreadyExists: - return "RenderWindow Creation Failed: Window by given id already exists." + return "MetalRenderWindow Creation Failed: Window by given id already exists." case .commandQueueCreationFailed: - return "RenderWindow Creation Failed: MTLDevice cannot create MTLCommandQueue." + return "MetalRenderWindow Creation Failed: MTLDevice cannot create MTLCommandQueue." } } } } - final class RenderWindow { - private(set) weak var view: MTKView? - var drawable: CAMetalDrawable? - var commandBuffer: MTLCommandBuffer? - - internal init( - view: MTKView? = nil, - commandBuffer: MTLCommandBuffer? = nil - ) { + struct MetalRenderWindow: Sendable { + let view: MTKView + var size: SizeInt + var scaleFactor: Float + + init(view: MTKView, size: SizeInt, scaleFactor: Float) { self.view = view - self.commandBuffer = commandBuffer - } - - func getRenderPass() -> MTLRenderPassDescriptor? { - guard let drawable else { - return nil - } - - let mtlRenderPass = MTLRenderPassDescriptor() - mtlRenderPass.colorAttachments[0].texture = drawable.texture - return mtlRenderPass + self.size = size + self.scaleFactor = scaleFactor } } } diff --git a/Sources/AdaRender/Backends/Metal/MetalRenderDevice.swift b/Sources/AdaRender/Backends/Metal/MetalRenderDevice.swift index 8b3b5abf5..87fdc407f 100644 --- a/Sources/AdaRender/Backends/Metal/MetalRenderDevice.swift +++ b/Sources/AdaRender/Backends/Metal/MetalRenderDevice.swift @@ -7,9 +7,11 @@ #if METAL import AdaUtils -import MetalKit +import Metal +@unsafe @preconcurrency import MetalKit +import Math -final class MetalRenderDevice: RenderDevice { +final class MetalRenderDevice: RenderDevice, @unchecked Sendable { let device: MTLDevice let commandQueue: MTLCommandQueue @@ -40,6 +42,10 @@ final class MetalRenderDevice: RenderDevice { return MetalFramebuffer(descriptor: descriptor) } + func createCommandQueue() -> CommandQueue { + return MetalCommandQueue(commandQueue: self.commandQueue) + } + // swiftlint:disable:next function_body_length func createRenderPipeline(from descriptor: RenderPipelineDescriptor) -> RenderPipeline { let pipelineDescriptor = MTLRenderPipelineDescriptor() @@ -90,7 +96,6 @@ final class MetalRenderDevice: RenderDevice { depthStencilDescriptor.isDepthWriteEnabled = depthStencilDesc.isDepthWriteEnabled if depthStencilDesc.isEnableStencil { - guard let stencilDesc = depthStencilDesc.stencilOperationDescriptor else { fatalError("StencilOperationDescriptor instance not passed to DepthStencilDescriptor object.") } @@ -142,104 +147,128 @@ final class MetalRenderDevice: RenderDevice { // MARK: - Buffers - func createIndexBuffer(format: IndexBufferFormat, bytes: UnsafeRawPointer, length: Int) -> IndexBuffer { + func createIndexBuffer(label: String?, format: IndexBufferFormat, bytes: UnsafeRawPointer, length: Int) -> IndexBuffer { let buffer = self.device.makeBuffer(length: length, options: .storageModeShared)! - buffer.contents().copyMemory(from: bytes, byteCount: length) - - return MetalIndexBuffer(buffer: buffer, indexFormat: format) + unsafe buffer.contents().copyMemory(from: bytes, byteCount: length) + let metalBuffer = MetalIndexBuffer(buffer: buffer, indexFormat: format) + metalBuffer.label = label + return metalBuffer } - func createVertexBuffer(length: Int, binding: Int) -> VertexBuffer { + func createVertexBuffer(label: String?, length: Int, binding: Int) -> VertexBuffer { let buffer = self.device.makeBuffer(length: length, options: .storageModeShared)! - return MetalVertexBuffer(buffer: buffer, binding: 0, offset: 0) + let metalBuffer = MetalVertexBuffer(buffer: buffer, binding: 0, offset: 0) + metalBuffer.label = label + return metalBuffer } - func createBuffer(length: Int, options: ResourceOptions) -> Buffer { + func createBuffer(label: String?, length: Int, options: ResourceOptions) -> Buffer { let buffer = self.device.makeBuffer(length: length, options: options.metal)! - return MetalBuffer(buffer: buffer) + let metalBuffer = MetalBuffer(buffer: buffer) + metalBuffer.label = label + return metalBuffer } - func createBuffer(bytes: UnsafeRawPointer, length: Int, options: ResourceOptions) -> Buffer { - let buffer = self.device.makeBuffer(bytes: bytes, length: length, options: options.metal)! - return MetalBuffer(buffer: buffer) + func createBuffer(label: String?, bytes: UnsafeRawPointer, length: Int, options: ResourceOptions) -> Buffer { + let buffer = unsafe self.device.makeBuffer(bytes: bytes, length: length, options: options.metal)! + let metalBuffer = MetalBuffer(buffer: buffer) + metalBuffer.label = label + return metalBuffer } -} - -// MARK: Texture -extension MetalRenderDevice { - func createTexture(from descriptor: TextureDescriptor) -> GPUTexture { - return MetalGPUTexture(descriptor: descriptor, device: self.device) - } + @MainActor + func createSwapchain(from window: WindowID) -> any Swapchain { + guard let context else { + fatalError("Context not found") + } + let window = context.getRenderWindow(for: window) + .unwrap(message: "RenderWindow not found") - func getImage(from texture: Texture) -> Image? { - (texture.gpuTexture as? MetalGPUTexture)?.getImage() + return require(window.view.layer as? CAMetalLayer, message: "Expected that view layer is CAMetalLayer") } } -// MARK: - Drawings - -extension MetalRenderDevice { +extension CAMetalLayer: Swapchain { + public var drawablePixelFormat: PixelFormat { + self.pixelFormat.toPixelFormat() + } - func beginDraw( - for window: WindowRef, - clearColor: Color, - loadAction: AttachmentLoadAction, - storeAction: AttachmentStoreAction - ) throws -> DrawList { - guard let context else { - throw DrawListError.notAGlobalDevice - } - guard let window = context.getRenderWindow(for: window) else { - throw DrawListError.windowNotExists + public func getNextDrawable(_ renderDevice: RenderDevice) -> (any Drawable)? { + guard + let drawable = self.nextDrawable(), + let mtlDevice = renderDevice as? MetalRenderDevice + else { + return nil } - guard let mtlRenderPass = window.getRenderPass() else { - throw DrawListError.failedToGetSurfaceTexture - } - - mtlRenderPass.colorAttachments[0].loadAction = loadAction.toMetal - mtlRenderPass.colorAttachments[0].storeAction = storeAction.toMetal - mtlRenderPass.colorAttachments[0].clearColor = clearColor.toMetalClearColor - guard let mtlCommandBuffer = self.commandQueue.makeCommandBuffer() else { - throw DrawListError.failedToCreateCommandBuffer + return MetalDrawable(drawable: drawable, commandQueue: mtlDevice.commandQueue) + } +} + +extension MTLPixelFormat { + func toPixelFormat() -> PixelFormat { + switch self { + case .bgra8Unorm: + return .bgra8 + case .bgra8Unorm_srgb: + return .bgra8_srgb + case .rgba8Unorm: + return .rgba8 + case .rgba8Uint: + return .rgba8 + case .rgba16Float: + return .rgba_16f + case .rgba32Float: + return .rgba_32f + case .depth32Float: + return .depth_32f + case .depth32Float_stencil8: + return .depth_32f_stencil8 + default: + fatalError("Unsupported pixel format: \(self)") } + } +} - let encoder = mtlCommandBuffer.makeRenderCommandEncoder(descriptor: mtlRenderPass)! - let commandBuffer = MetalRenderCommandBuffer( - encoder: encoder, - commandBuffer: mtlCommandBuffer - ) +final class MetalDrawable: Drawable { + private let commandQueue: MTLCommandQueue + private let mtlDrawable: CAMetalDrawable - return DrawList(commandBuffer: commandBuffer, renderDevice: self) + public var texture: any GPUTexture { + MetalGPUTexture(texture: self.mtlDrawable.texture) } - func beginDraw(to framebuffer: Framebuffer, clearColors: [Color]?) throws -> DrawList { - guard let mtlCommandBuffer = self.commandQueue.makeCommandBuffer() else { - throw DrawListError.failedToCreateCommandBuffer - } - guard let mtlRenderPassDesc = (framebuffer as? MetalFramebuffer)?.renderPassDescriptor else { - throw DrawListError.failedToGetRenderPass + public func present() throws { + guard let commandBuffer = commandQueue.makeCommandBuffer() else { + return } + commandBuffer.label = "(AdaRender internal) Present" + commandBuffer.present(self.mtlDrawable) + commandBuffer.commit() + } - if let clearColors { - for (index, color) in clearColors.enumerated() { - mtlRenderPassDesc.colorAttachments[index].clearColor = color.toMetalClearColor - } - } + init(drawable: CAMetalDrawable, commandQueue: MTLCommandQueue) { + self.commandQueue = commandQueue + self.mtlDrawable = drawable + } +} - let encoder = mtlCommandBuffer.makeRenderCommandEncoder(descriptor: mtlRenderPassDesc)! - let commandBuffer = MetalRenderCommandBuffer( - encoder: encoder, - commandBuffer: mtlCommandBuffer - ) +// MARK: Texture + +extension MetalRenderDevice { + func createTexture(from descriptor: TextureDescriptor) -> GPUTexture { + return MetalGPUTexture(descriptor: descriptor, device: self.device) + } - return DrawList(commandBuffer: commandBuffer, renderDevice: self) + func getImage(from texture: Texture) -> Image? { + (texture.gpuTexture as? MetalGPUTexture)?.getImage() } +} - // MARK: - Uniforms - +// MARK: - Drawings +extension MetalRenderDevice { func createUniformBufferSet() -> UniformBufferSet { - return GenericUniformBufferSet(frames: RenderEngine.configurations.maxFramesInFlight, device: self) + return unsafe GenericUniformBufferSet(frames: RenderEngine.configurations.maxFramesInFlight, device: self) } func createUniformBuffer(length: Int, binding: Int) -> UniformBuffer { diff --git a/Sources/AdaRender/Backends/Metal/Misc/MetalBlitCommandEncoder.swift b/Sources/AdaRender/Backends/Metal/Misc/MetalBlitCommandEncoder.swift new file mode 100644 index 000000000..67561cde1 --- /dev/null +++ b/Sources/AdaRender/Backends/Metal/Misc/MetalBlitCommandEncoder.swift @@ -0,0 +1,157 @@ +// +// MetalBlitCommandEncoder.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 23.11.2025. +// + +#if canImport(Metal) +import Foundation +import Metal + +final class MetalBlitCommandEncoder: BlitCommandEncoder { + let blitEncoder: MTLBlitCommandEncoder + + init(blitEncoder: MTLBlitCommandEncoder) { + self.blitEncoder = blitEncoder + } + + func pushDebugName(_ string: String) { + blitEncoder.pushDebugGroup(string) + } + + func popDebugName() { + blitEncoder.popDebugGroup() + } + + func copyTextureToTexture( + source: Texture, + sourceOrigin: Origin3D, + sourceSize: Size3D, + sourceMipLevel: Int, + sourceSlice: Int, + destination: Texture, + destinationOrigin: Origin3D, + destinationMipLevel: Int, + destinationSlice: Int + ) { + guard + let src = source.gpuTexture as? MetalGPUTexture, + let dst = destination.gpuTexture as? MetalGPUTexture + else { fatalError("Textures must be Metal textures") } + + blitEncoder.copy( + from: src.texture, + sourceSlice: sourceSlice, + sourceLevel: sourceMipLevel, + sourceOrigin: MTLOrigin(x: sourceOrigin.x, y: sourceOrigin.y, z: sourceOrigin.z), + sourceSize: MTLSize(width: sourceSize.width, height: sourceSize.height, depth: sourceSize.depth), + to: dst.texture, + destinationSlice: destinationSlice, + destinationLevel: destinationMipLevel, + destinationOrigin: MTLOrigin(x: destinationOrigin.x, y: destinationOrigin.y, z: destinationOrigin.z) + ) + } + + func copyBufferToBuffer( + source: Buffer, + sourceOffset: Int, + destination: Buffer, + destinationOffset: Int, + size: Int + ) { + guard + let src = source as? MetalBuffer, + let dst = destination as? MetalBuffer + else { fatalError("Buffers must be Metal buffers") } + + blitEncoder.copy( + from: src.buffer, + sourceOffset: sourceOffset, + to: dst.buffer, + destinationOffset: destinationOffset, + size: size + ) + } + + func copyBufferToTexture( + source: Buffer, + sourceOffset: Int, + sourceBytesPerRow: Int, + sourceBytesPerImage: Int, + sourceSize: Size3D, + destination: Texture, + destinationOrigin: Origin3D, + destinationMipLevel: Int, + destinationSlice: Int + ) { + guard + let src = source as? MetalBuffer, + let dst = destination.gpuTexture as? MetalGPUTexture + else { fatalError("Invalid Metal resources") } + + blitEncoder.copy( + from: src.buffer, + sourceOffset: sourceOffset, + sourceBytesPerRow: sourceBytesPerRow, + sourceBytesPerImage: sourceBytesPerImage, + sourceSize: MTLSize(width: sourceSize.width, height: sourceSize.height, depth: sourceSize.depth), + to: dst.texture, + destinationSlice: destinationSlice, + destinationLevel: destinationMipLevel, + destinationOrigin: MTLOrigin(x: destinationOrigin.x, y: destinationOrigin.y, z: destinationOrigin.z) + ) + } + + func copyTextureToBuffer( + source: Texture, + sourceOrigin: Origin3D, + sourceMipLevel: Int, + sourceSlice: Int, + sourceSize: Size3D, + destination: Buffer, + destinationOffset: Int, + destinationBytesPerRow: Int, + destinationBytesPerImage: Int + ) { + guard + let src = source.gpuTexture as? MetalGPUTexture, + let dst = destination as? MetalBuffer + else { fatalError("Invalid Metal resources") } + + blitEncoder.copy( + from: src.texture, + sourceSlice: sourceSlice, + sourceLevel: sourceMipLevel, + sourceOrigin: MTLOrigin(x: sourceOrigin.x, y: sourceOrigin.y, z: sourceOrigin.z), + sourceSize: MTLSize(width: sourceSize.width, height: sourceSize.height, depth: sourceSize.depth), + to: dst.buffer, + destinationOffset: destinationOffset, + destinationBytesPerRow: destinationBytesPerRow, + destinationBytesPerImage: destinationBytesPerImage + ) + } + + func generateMipmaps(for texture: Texture) { + guard let tex = texture.gpuTexture as? MetalGPUTexture else { + fatalError("Texture must be a Metal texture") + } + blitEncoder.generateMipmaps(for: tex.texture) + } + + func fillBuffer(_ buffer: Buffer, range: Range, value: UInt8) { + guard let metalBuffer = buffer as? MetalBuffer else { + fatalError("Buffer must be a Metal buffer") + } + blitEncoder.__fill( + metalBuffer.buffer, + range: NSRange(location: range.lowerBound, length: range.count), + value: value + ) + } + + func endBlitPass() { + blitEncoder.endEncoding() + } +} +#endif diff --git a/Sources/AdaRender/Backends/Metal/Misc/MetalBuffer.swift b/Sources/AdaRender/Backends/Metal/Misc/MetalBuffer.swift index 2f5ed7ce2..0ffdc1954 100644 --- a/Sources/AdaRender/Backends/Metal/Misc/MetalBuffer.swift +++ b/Sources/AdaRender/Backends/Metal/Misc/MetalBuffer.swift @@ -6,7 +6,7 @@ // #if METAL -@preconcurrency import Metal +@unsafe @preconcurrency import Metal class MetalBuffer: Buffer, @unchecked Sendable { let buffer: MTLBuffer @@ -30,7 +30,7 @@ class MetalBuffer: Buffer, @unchecked Sendable { func contents() -> UnsafeMutableRawPointer { return self.buffer.contents() } func setData(_ bytes: UnsafeMutableRawPointer, byteCount: Int, offset: Int) { - self.buffer.contents().advanced(by: offset).copyMemory(from: bytes, byteCount: byteCount) + unsafe self.buffer.contents().advanced(by: offset).copyMemory(from: bytes, byteCount: byteCount) } } diff --git a/Sources/AdaRender/Backends/Metal/Misc/MetalCommandEncoder.swift b/Sources/AdaRender/Backends/Metal/Misc/MetalCommandEncoder.swift new file mode 100644 index 000000000..0d94af807 --- /dev/null +++ b/Sources/AdaRender/Backends/Metal/Misc/MetalCommandEncoder.swift @@ -0,0 +1,66 @@ +// +// MetalCommandEncoder.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 23.11.2025. +// + +#if canImport(Metal) +import AdaUtils +import Math +import Metal + +final class MetalCommandEncoder: CommandBuffer { + let commandBuffer: MTLCommandBuffer + + init(commandBuffer: MTLCommandBuffer) { + self.commandBuffer = commandBuffer + } + + func commit() { + self.commandBuffer.commit() + } + + func beginRenderPass(_ desc: RenderPassDescriptor) -> RenderCommandEncoder { + let renderPassDescriptor = MTLRenderPassDescriptor() + let attachments = desc.colorAttachments + + for (index, attachment) in attachments.enumerated() { + let colorAttachment = renderPassDescriptor.colorAttachments[index] + colorAttachment?.texture = (attachment.texture.gpuTexture as! MetalGPUTexture).texture + colorAttachment?.loadAction = attachment.operation?.loadAction.toMetal ?? .dontCare + colorAttachment?.storeAction = attachment.operation?.storeAction.toMetal ?? .dontCare + colorAttachment?.clearColor = attachment.clearColor?.toMetalClearColor ?? Color.black.toMetalClearColor + } + + if let depthStencilAttachment = desc.depthStencilAttachment { + renderPassDescriptor.depthAttachment.texture = (depthStencilAttachment.texture.gpuTexture as! MetalGPUTexture).texture + renderPassDescriptor.depthAttachment.loadAction = depthStencilAttachment.depthOperation?.loadAction.toMetal ?? .dontCare + renderPassDescriptor.depthAttachment.storeAction = depthStencilAttachment.depthOperation?.storeAction.toMetal ?? .dontCare + // renderPassDescriptor.depthAttachment.clearDepth = Double(depthStencilAttachment.depthOperation?.clearDepth ?? 0) + // renderPassDescriptor.depthAttachment.clearStencil = UInt32(depthStencilAttachment.stencilOperation?.clearStencil ?? 0) + renderPassDescriptor.stencilAttachment.texture = (depthStencilAttachment.texture.gpuTexture as! MetalGPUTexture).texture + renderPassDescriptor.stencilAttachment.loadAction = depthStencilAttachment.stencilOperation?.loadAction.toMetal ?? .dontCare + renderPassDescriptor.stencilAttachment.storeAction = depthStencilAttachment.stencilOperation?.storeAction.toMetal ?? .dontCare + // renderPassDescriptor.stencilAttachment.clearStencil = UInt32(depthStencilAttachment.stencilOperation?.clearStencil ?? 0) + } + + guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + fatalError("Failed to create MTLRenderCommandEncoder") + } + encoder.label = desc.label + + return MetalRenderCommandEncoder( + renderEncoder: encoder + ) + } + + func beginBlitPass(_ desc: BlitPassDescriptor) -> BlitCommandEncoder { + guard let encoder = commandBuffer.makeBlitCommandEncoder() else { + fatalError("Failed to create MTLBlitCommandEncoder") + } + encoder.label = desc.label + return MetalBlitCommandEncoder(blitEncoder: encoder) + } +} +#endif diff --git a/Sources/AdaRender/Backends/Metal/Misc/MetalCommandQueue.swift b/Sources/AdaRender/Backends/Metal/Misc/MetalCommandQueue.swift new file mode 100644 index 000000000..b34c0f541 --- /dev/null +++ b/Sources/AdaRender/Backends/Metal/Misc/MetalCommandQueue.swift @@ -0,0 +1,25 @@ +// +// MetalCommandQueue.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 23.11.2025. +// + +#if canImport(Metal) +import Metal + +final class MetalCommandQueue: CommandQueue { + let commandQueue: MTLCommandQueue + + init(commandQueue: MTLCommandQueue) { + self.commandQueue = commandQueue + } + + func makeCommandBuffer() -> CommandBuffer { + guard let commandBuffer = commandQueue.makeCommandBuffer() else { + fatalError("MetalCommandQueue failed. Can't create MTLCommandBuffer.") + } + return MetalCommandEncoder(commandBuffer: commandBuffer) + } +} +#endif diff --git a/Sources/AdaRender/Backends/Metal/Misc/MetalGPUTexture.swift b/Sources/AdaRender/Backends/Metal/Misc/MetalGPUTexture.swift index 0ff217bc1..b32283f27 100644 --- a/Sources/AdaRender/Backends/Metal/Misc/MetalGPUTexture.swift +++ b/Sources/AdaRender/Backends/Metal/Misc/MetalGPUTexture.swift @@ -6,9 +6,14 @@ // #if METAL +import Math import Metal final class MetalGPUTexture: GPUTexture { + var size: SizeInt { + SizeInt(width: self.texture.width, height: self.texture.height) + } + var texture: MTLTexture init(texture: MTLTexture) { @@ -72,10 +77,10 @@ final class MetalGPUTexture: GPUTexture { let bytesPerRow = descriptor.pixelFormat.bytesPerComponent * image.width - image.data.withUnsafeBytes { buffer in - precondition(buffer.baseAddress != nil, "Image should not contains empty address.") + unsafe image.data.withUnsafeBytes { buffer in + unsafe precondition(buffer.baseAddress != nil, "Image should not contains empty address.") - texture.replace( + unsafe texture.replace( region: region, mipmapLevel: 0, withBytes: buffer.baseAddress!, @@ -109,7 +114,7 @@ final class MetalGPUTexture: GPUTexture { let pixelCount = self.texture.width * self.texture.height var imageBytes = [UInt8](repeating: 0, count: pixelCount * bytesInPixel) - self.texture.getBytes( + unsafe self.texture.getBytes( &imageBytes, bytesPerRow: bytesPerRow, from: MTLRegion( diff --git a/Sources/AdaRender/Backends/Metal/Misc/MetalRenderCommandEncoder.swift b/Sources/AdaRender/Backends/Metal/Misc/MetalRenderCommandEncoder.swift new file mode 100644 index 000000000..c3b91974b --- /dev/null +++ b/Sources/AdaRender/Backends/Metal/Misc/MetalRenderCommandEncoder.swift @@ -0,0 +1,164 @@ +// +// MetalRenderCommandEncoder.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 23.11.2025. +// + +#if canImport(Metal) +import Math +import Metal + +final class MetalRenderCommandEncoder: RenderCommandEncoder { + + let renderEncoder: MTLRenderCommandEncoder + private var currentIndexBuffer: MTLBuffer? + private var currentIndexType: MTLIndexType = .uint32 + + init(renderEncoder: MTLRenderCommandEncoder) { + self.renderEncoder = renderEncoder + } + + func pushDebugName(_ string: String) { + renderEncoder.pushDebugGroup(string) + } + + func popDebugName() { + renderEncoder.popDebugGroup() + } + + func setRenderPipelineState(_ pipeline: RenderPipeline) { + guard let metalPipeline = pipeline as? MetalRenderPipeline else { + fatalError("RenderPipeline is not a MetalRenderPipeline") + } + renderEncoder.setRenderPipelineState(metalPipeline.renderPipeline) + } + + func setVertexBuffer(_ buffer: UniformBuffer, offset: Int, index: Int) { + guard let metalBuffer = buffer as? MetalUniformBuffer else { + fatalError("UniformBuffer is not a MetalUniformBuffer") + } + renderEncoder.setVertexBuffer(metalBuffer.buffer, offset: offset, index: index) + } + + func setVertexBuffer(_ buffer: VertexBuffer, offset: Int, index: Int) { + guard let metalBuffer = buffer as? MetalVertexBuffer else { + fatalError("VertexBuffer is not a MetalVertexBuffer") + } + renderEncoder.setVertexBuffer(metalBuffer.buffer, offset: offset, index: index) + } + + func setFragmentBuffer(_ buffer: UniformBuffer, offset: Int, index: Int) { + guard let metalBuffer = buffer as? MetalUniformBuffer else { + fatalError("UniformBuffer is not a MetalUniformBuffer") + } + renderEncoder.setFragmentBuffer(metalBuffer.buffer, offset: offset, index: index) + } + + + func setVertexBuffer(_ bufferData: BufferData, offset: Int, index: Int) { + guard let metalBuffer = bufferData.buffer as? MetalBuffer else { + fatalError("BufferData is not a MetalBuffer") + } + + renderEncoder.setVertexBuffer(metalBuffer.buffer, offset: offset, index: index) + } + + func setFragmentBuffer(_ bufferData: BufferData, offset: Int, index: Int) { + guard let metalBuffer = bufferData.buffer as? MetalBuffer else { + fatalError("UniformBuffer is not a MetalUniformBuffer") + } + + renderEncoder.setFragmentBuffer(metalBuffer.buffer, offset: offset, index: index) + } + + func setIndexBuffer(_ bufferData: BufferData, indexFormat: IndexBufferFormat) { + guard let metalBuffer = bufferData.buffer as? MetalBuffer else { + fatalError("UniformBuffer is not a MetalUniformBuffer") + } + currentIndexBuffer = metalBuffer.buffer + currentIndexType = indexFormat == .uInt32 ? .uint32 : .uint16 + } + + func setVertexBytes(_ bytes: UnsafeRawPointer, length: Int, index: Int) { + unsafe renderEncoder.setVertexBytes(bytes, length: length, index: index) + } + + func setFragmentTexture(_ texture: Texture, index: Int) { + guard let metalTexture = texture.gpuTexture as? MetalGPUTexture else { + fatalError("Texture's gpuTexture is not a MetalGPUTexture") + } + renderEncoder.setFragmentTexture(metalTexture.texture, index: index) + } + + func setFragmentSamplerState(_ sampler: Sampler, index: Int) { + guard let metalSampler = sampler as? MetalSampler else { + fatalError("Sampler is not a MetalSampler") + } + renderEncoder.setFragmentSamplerState(metalSampler.mtlSampler, index: index) + } + + func setViewport(_ viewport: Rect) { + renderEncoder.setViewport( + MTLViewport( + originX: Double(viewport.origin.x), + originY: Double(viewport.origin.y), + width: Double(viewport.size.width), + height: Double(viewport.size.height), + znear: 0, + zfar: 1 + ) + ) + } + + func setScissorRect(_ rect: Rect) { + renderEncoder.setScissorRect( + MTLScissorRect( + x: Int(rect.origin.x), + y: Int(rect.origin.y), + width: Int(rect.size.width), + height: Int(rect.size.height) + ) + ) + } + + func setTriangleFillMode(_ fillMode: TriangleFillMode) { + renderEncoder.setTriangleFillMode(fillMode == .fill ? .fill : .lines) + } + + func setIndexBuffer(_ buffer: IndexBuffer, offset: Int) { + guard let metalIndexBuffer = buffer as? MetalIndexBuffer else { + fatalError("IndexBuffer is not a MetalIndexBuffer") + } + self.currentIndexBuffer = metalIndexBuffer.buffer + self.currentIndexType = (metalIndexBuffer.indexFormat == .uInt32) ? .uint32 : .uint16 + } + + func drawIndexed(indexCount: Int, indexBufferOffset: Int, instanceCount: Int) { + guard let indexBuffer = self.currentIndexBuffer else { + fatalError("Index buffer is not set. Call setIndexBuffer(_:offset:) before drawIndexed().") + } + renderEncoder.drawIndexedPrimitives( + type: .triangle, + indexCount: indexCount, + indexType: self.currentIndexType, + indexBuffer: indexBuffer, + indexBufferOffset: indexBufferOffset, + instanceCount: instanceCount + ) + } + + func draw(type: IndexPrimitive, vertexStart: Int, vertexCount: Int, instanceCount: Int) { + renderEncoder.drawPrimitives( + type: type.toMetal, + vertexStart: vertexStart, + vertexCount: vertexCount, + instanceCount: instanceCount + ) + } + + func endRenderPass() { + renderEncoder.endEncoding() + } +} +#endif diff --git a/Sources/AdaRender/Backends/Metal/Misc/MetalRenderPipeline.swift b/Sources/AdaRender/Backends/Metal/Misc/MetalRenderPipeline.swift index e4c973f85..4766a18de 100644 --- a/Sources/AdaRender/Backends/Metal/Misc/MetalRenderPipeline.swift +++ b/Sources/AdaRender/Backends/Metal/Misc/MetalRenderPipeline.swift @@ -6,7 +6,7 @@ // #if METAL -@preconcurrency import Metal +@unsafe @preconcurrency import Metal final class MetalRenderPipeline: RenderPipeline { diff --git a/Sources/AdaRender/Backends/Metal/Misc/MetalSampler.swift b/Sources/AdaRender/Backends/Metal/Misc/MetalSampler.swift index 386f30923..7c09ff3ba 100644 --- a/Sources/AdaRender/Backends/Metal/Misc/MetalSampler.swift +++ b/Sources/AdaRender/Backends/Metal/Misc/MetalSampler.swift @@ -6,9 +6,9 @@ // #if METAL -import Metal +@unsafe @preconcurrency import Metal -final class MetalSampler: Sampler { +final class MetalSampler: Sampler, Sendable { let descriptor: SamplerDescriptor let mtlSampler: MTLSamplerState diff --git a/Sources/AdaRender/Backends/Misc/GenericUniformBufferSet.swift b/Sources/AdaRender/Backends/Misc/GenericUniformBufferSet.swift index 823612ef5..11152671d 100644 --- a/Sources/AdaRender/Backends/Misc/GenericUniformBufferSet.swift +++ b/Sources/AdaRender/Backends/Misc/GenericUniformBufferSet.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 3/18/24. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif final class GenericUniformBufferSet: UniformBufferSet, @unchecked Sendable { /// Max frames in flight. diff --git a/Sources/AdaRender/Backends/OpenGL/OpenGLBackend.swift b/Sources/AdaRender/Backends/OpenGL/OpenGLBackend.swift index 6f7ec9432..90f95b10f 100644 --- a/Sources/AdaRender/Backends/OpenGL/OpenGLBackend.swift +++ b/Sources/AdaRender/Backends/OpenGL/OpenGLBackend.swift @@ -16,7 +16,11 @@ import OpenGL.GL3 import OpenGL #endif import Math +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif #if DARWIN private let GL_DEBUG_OUTPUT = GLenum(0x92E0) diff --git a/Sources/AdaRender/Backends/RenderBackend.swift b/Sources/AdaRender/Backends/RenderBackend.swift index df385e316..b6c1d8144 100644 --- a/Sources/AdaRender/Backends/RenderBackend.swift +++ b/Sources/AdaRender/Backends/RenderBackend.swift @@ -5,23 +5,27 @@ // Created by v.prusakov on 10/9/21. // +import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Math -import AdaUtils public enum TriangleFillMode { case fill case lines } -public enum RenderBackendType: String { +public enum RenderBackendType: String, Sendable { case opengl case metal case vulkan } /// This protocol describe interface for GPU. -protocol RenderBackend: AnyObject { +protocol RenderBackend: AnyObject, Sendable { var type: RenderBackendType { get } @@ -37,39 +41,38 @@ protocol RenderBackend: AnyObject { /// Register a new render window for render backend. /// Window in this case is entity that managed a drawables (aka swapchain). /// - Throws: Throw error if something went wrong. - @MainActor func createWindow(_ windowId: WindowRef, for surface: RenderSurface, size: SizeInt) throws + @MainActor func createWindow(_ windowId: WindowID, for surface: RenderSurface, size: SizeInt) throws /// Resize registred render window. /// - Throws: Throw error if window is not registred. - @MainActor func resizeWindow(_ windowId: WindowRef, newSize: SizeInt) throws + @MainActor func resizeWindow(_ windowId: WindowID, newSize: SizeInt) throws /// Destroy render window from render backend. /// - Throws: Throw error if window is not registred. - @MainActor func destroyWindow(_ windowId: WindowRef) throws + @MainActor func destroyWindow(_ windowId: WindowID) throws - /// Begin rendering a frame for all windows. - @MainActor func beginFrame() throws + @MainActor func getRenderWindow(for windowId: WindowID) -> RenderWindow? - /// Release any data associated with the current frame. - @MainActor func endFrame() throws + /// Returns render windows + @MainActor func getRenderWindows() throws -> RenderWindows } /// The GPU device instance resposible for rendering and computing. -public protocol RenderDevice: AnyObject { +public protocol RenderDevice: AnyObject, Sendable { // MARK: - Buffers /// Create a new GPU buffer with specific length and options. - func createBuffer(length: Int, options: ResourceOptions) -> Buffer + func createBuffer(label: String?, length: Int, options: ResourceOptions) -> Buffer /// Create a new GPU buffer with specific data, length and options. - func createBuffer(bytes: UnsafeRawPointer, length: Int, options: ResourceOptions) -> Buffer + func createBuffer(label: String?, bytes: UnsafeRawPointer, length: Int, options: ResourceOptions) -> Buffer /// Create a new index buffer with specific index, format, data and length. - func createIndexBuffer(format: IndexBufferFormat, bytes: UnsafeRawPointer, length: Int) -> IndexBuffer + func createIndexBuffer(label: String?, format: IndexBufferFormat, bytes: UnsafeRawPointer, length: Int) -> IndexBuffer /// Create a new vertex buffer for specific length and binding. - func createVertexBuffer(length: Int, binding: Int) -> VertexBuffer + func createVertexBuffer(label: String?, length: Int, binding: Int) -> VertexBuffer // MARK: - Shaders @@ -102,30 +105,47 @@ public protocol RenderDevice: AnyObject { /// Get image from texture rid. func getImage(from texture: Texture) -> Image? - // MARK: - Draw - - /// Begin draw for window. - /// - Warning: Local RenderDevice can't render on specific window. Instead, use global ``RenderEngine/renderDevice`` instance. - /// - Returns: ``DrawList`` which contains information about drawing. - func beginDraw( - for window: WindowRef, - clearColor: Color, - loadAction: AttachmentLoadAction, - storeAction: AttachmentStoreAction - ) throws -> DrawList - - /// Begin draw to framebuffer. - /// - Returns: ``DrawList`` which contains information about drawing. - func beginDraw(to framebuffer: Framebuffer, clearColors: [Color]?) throws -> DrawList - - /// Draw all items from ``DrawList``. - /// - Parameter indexCount: For each instance, the number of indices to read from the index buffer. - /// - Parameter indexBufferOffset: Byte offset within indexBuffer to start reading indices from. - /// - Parameter instanceCount: The number of instances to draw. - func draw(_ list: DrawList, indexCount: Int, indexBufferOffset: Int, instanceCount: Int) - - /// Commit all draws from ``DrawList``. - func endDrawList(_ drawList: DrawList) + func createCommandQueue() -> CommandQueue + + /// Create a new swapchain for specific window. + @MainActor + func createSwapchain(from window: WindowID) -> Swapchain +} + +public protocol Swapchain: AnyObject, Sendable { + var drawablePixelFormat: PixelFormat { get } + func getNextDrawable(_ renderDevice: RenderDevice) -> (any Drawable)? +} + +public protocol Drawable: AnyObject, Sendable { + var texture: any GPUTexture { get } + func present() throws +} + +public extension RenderDevice { + /// Create a new GPU buffer with specific length and options. + @inline(__always) + func createBuffer(length: Int, options: ResourceOptions) -> Buffer { + createBuffer(label: nil, length: length, options: options) + } + + /// Create a new GPU buffer with specific data, length and options. + @inline(__always) + func createBuffer(bytes: UnsafeRawPointer, length: Int, options: ResourceOptions) -> Buffer { + unsafe createBuffer(label: nil, bytes: bytes, length: length, options: options) + } + + /// Create a new index buffer with specific index, format, data and length. + @inline(__always) + func createIndexBuffer(format: IndexBufferFormat, bytes: UnsafeRawPointer, length: Int) -> IndexBuffer { + unsafe createIndexBuffer(label: nil, format: format, bytes: bytes, length: length) + } + + /// Create a new vertex buffer for specific length and binding. + @inline(__always) + func createVertexBuffer(length: Int, binding: Int) -> VertexBuffer { + createVertexBuffer(label: nil, length: length, binding: binding) + } } enum DrawListError: String, LocalizedError { diff --git a/Sources/AdaRender/Backends/Vulkan/VulkanRenderBackend.swift b/Sources/AdaRender/Backends/Vulkan/VulkanRenderBackend.swift index 668270db1..7bd424c4e 100644 --- a/Sources/AdaRender/Backends/Vulkan/VulkanRenderBackend.swift +++ b/Sources/AdaRender/Backends/Vulkan/VulkanRenderBackend.swift @@ -6,7 +6,11 @@ // #if VULKAN +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import CVulkan import Vulkan import Math diff --git a/Sources/AdaRender/Backends/Vulkan/VulkanRenderContext.swift b/Sources/AdaRender/Backends/Vulkan/VulkanRenderContext.swift index c26d36b8d..46f9d405f 100644 --- a/Sources/AdaRender/Backends/Vulkan/VulkanRenderContext.swift +++ b/Sources/AdaRender/Backends/Vulkan/VulkanRenderContext.swift @@ -6,7 +6,11 @@ // #if VULKAN +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Vulkan import CVulkan import Math diff --git a/Sources/AdaRender/BatchTransparent2DItemsSystem.swift b/Sources/AdaRender/BatchTransparent2DItemsSystem.swift index e5a38b98e..e8cb2f4fc 100644 --- a/Sources/AdaRender/BatchTransparent2DItemsSystem.swift +++ b/Sources/AdaRender/BatchTransparent2DItemsSystem.swift @@ -9,7 +9,7 @@ import AdaECS /// Batch transparent items which contains batchEntity. /// Run each frame before drawing. -@System +@PlainSystem public struct BatchTransparent2DItemsSystem { @Query>> @@ -17,7 +17,7 @@ public struct BatchTransparent2DItemsSystem { public init(world: World) { } - public func update(context: inout UpdateContext) { + public func update(context: UpdateContext) { self.query.forEach { renderItems in let items = renderItems.wrappedValue.sorted().items var batchedItems: [Transparent2DRenderItem] = [] @@ -45,7 +45,7 @@ public struct BatchTransparent2DItemsSystem { return false } - if otherItem.batchEntity.id != currentItem.batchEntity.id { + if otherItem.batchEntity != currentItem.batchEntity { return false } diff --git a/Sources/AdaRender/BoundingComponent.swift b/Sources/AdaRender/BoundingComponent.swift index 24a3558fc..7849935fd 100644 --- a/Sources/AdaRender/BoundingComponent.swift +++ b/Sources/AdaRender/BoundingComponent.swift @@ -6,6 +6,7 @@ // import AdaECS +import AdaUtils import Math /// Contains information about bounds of entity. @@ -22,3 +23,8 @@ public struct BoundingComponent { self.bounds = bounds } } + +extension BoundingComponent: DefaultValue { + public static let defaultValue: BoundingComponent = .init(bounds: .aabb(.empty)) +} + diff --git a/Sources/AdaRender/CameraComponent.swift b/Sources/AdaRender/CameraComponent.swift index d15807dfa..183adf72c 100644 --- a/Sources/AdaRender/CameraComponent.swift +++ b/Sources/AdaRender/CameraComponent.swift @@ -15,11 +15,24 @@ public enum WindowRef: Codable, Sendable, Hashable { /// The primary window. case primary /// The window id. - case windowId(RID) + case windowId(WindowID) } +public extension WindowRef { + func getWindowId(from primary: PrimaryWindowId) -> WindowID { + switch self { + case .primary: + primary.windowId + case .windowId(let windowID): + windowID + } + } +} + +public typealias WindowID = RID + /// A viewport. -public struct Viewport: Codable, Equatable { +public struct Viewport: Codable, Equatable, Sendable { /// The rectangle of the viewport. public var rect: Rect /// The depth range of the viewport. @@ -81,38 +94,29 @@ public struct Camera: Sendable { // MARK: Properties /// The closest point relative to camera that drawing will occur. - @Export public var near: Float = -1 /// The closest point relative to camera that drawing will occur - @Export public var far: Float = 1 /// Angle of camera view - @Export public var fieldOfView: Angle = .degrees(70) /// Base projection in camera - @Export public var projection: Projection = .perspective - @Export public var viewport: Viewport? /// Set camera is active - @Export - public var isActive = false + public var isActive = true /// Fill color for unused pixel. - @Export public var backgroundColor: Color = .surfaceClearColor /// Contains information about clear flags. /// By default contains ``CameraClearFlags/solid`` flag which fill clear color by ``Camera/backgroundColor``. - @Export public var clearFlags: CameraClearFlags = .solid - @Export @MinValue(0.1) public var orthographicScale: Float = 1 @@ -124,11 +128,13 @@ public struct Camera: Sendable { /// The computed data for the camera. @_spi(Internal) @NoExport - public var computedData: CameraComputedData + public var computedData: CameraComputedData = .defaultValue /// The render order. public var renderOrder: Int = 0 + public var viewMatrix: Transform3D = .identity + // MARK: - Init /// Create a new camera component with specific render target and viewport. @@ -148,8 +154,6 @@ public struct Camera: Sendable { public init(window: WindowRef) { self.renderTarget = .window(window) } - - public var viewMatrix: Transform3D = .identity } public extension Camera { @@ -205,7 +209,6 @@ public extension Camera { guard let viewport = self.viewport else { return nil } - let size = viewport.rect.size.asVector2 let ndcSpace = self.worldToNdc(cameraGlobalTransform: cameraGlobalTransform, worldPosition: worldPosition) @@ -229,7 +232,7 @@ extension Camera { /// The view matrix. public var viewMatrix: Transform3D = .identity /// The frustum. - public var frustum: Frustum = Frustum() + public var frustum: Frustum = .defaultValue /// The target scale factor. public var targetScaleFactor: Float = 1 } @@ -273,6 +276,10 @@ public struct GlobalViewUniformBufferSet { public init(label: String = "Global View Uniform") { self.uniformBufferSet = RenderEngine.shared.renderDevice.createUniformBufferSet() self.uniformBufferSet.label = label - self.uniformBufferSet.initBuffers(for: GlobalViewUniform.self, binding: GlobalBufferIndex.viewUniform, set: 0) + self.uniformBufferSet.initBuffers( + for: GlobalViewUniform.self, + binding: GlobalBufferIndex.viewUniform, + set: 0 + ) } } diff --git a/Sources/AdaRender/CameraPlugin.swift b/Sources/AdaRender/CameraPlugin.swift index 193c79ff8..1c81e5df1 100644 --- a/Sources/AdaRender/CameraPlugin.swift +++ b/Sources/AdaRender/CameraPlugin.swift @@ -6,44 +6,96 @@ // import AdaApp +import AdaAssets import AdaECS import AdaTransform import AdaUtils +import Logging +import Math public struct CameraPlugin: Plugin { public init() {} public func setup(in app: AppWorlds) { - Camera.registerComponent() - app.addSystem(CameraSystem.self) + Camera.registerComponent() + app.addSystem(CameraSystem.self, on: .preUpdate) guard let renderWorld = app.getSubworldBuilder(by: .renderWorld) else { return } - renderWorld.addSystem(ExtractCameraSystem.self) + renderWorld + .addSystem(ExtractCameraSystem.self, on: .extract) + .addSystem(ConfigurateRenderViewTargetSystem.self, on: .prepare) + .getRefResource(RenderGraph.self) + .wrappedValue + .addNode(CameraRenderNode()) + } +} - Task { - await renderWorld.mainWorld - .getResource(RenderGraph.self)? - .addNode(CameraRenderNode()) +@Component +public struct RenderViewTarget: @unchecked Sendable { + public var mainTexture: RenderTexture? + public var outputTexture: RenderTexture? + + public init() {} +} + +@System +func ConfigurateRenderViewTarget( + _ query: Query>, + _ surfaces: Res, + _ renderDevice: Res +) { + let logger = Logger(label: "ConfigurateRenderViewTarget") + query.forEach { entity, camera, renderViewTarget in + let viewportSize = camera.viewport?.rect.size.toSizeInt() ?? SizeInt(width: 800, height: 600) + + if renderViewTarget.mainTexture == nil { + renderViewTarget.mainTexture = RenderTexture( + size: viewportSize, + scaleFactor: camera.computedData.targetScaleFactor, + format: .bgra8, + debugLabel: "Camera Main Texture" + ) + } + + switch camera.renderTarget { + case .texture(let asset): + renderViewTarget.outputTexture = asset.asset + case .window(let ref): + guard + let surface = surfaces.windows[ref], + let swapchain = surface.swapchain, + let drawable = surface.currentDrawable + else { + logger.error("Failed to configurate render view target for window \(ref)") + return + } + renderViewTarget.outputTexture = RenderTexture( + gpuTexture: drawable.texture, + format: swapchain.drawablePixelFormat + ) } } } struct CameraRenderNode: RenderNode { - - static let query = EntityQuery(where: .has(Camera.self) && .has(Transform.self)) - - func execute(context: Context) async -> [RenderSlotValue] { - await context.world.performQuery(Self.query).concurrent.forEach { entity in - let compontents = entity.components - guard let camera = compontents[Camera.self], camera.isActive else { + @Query + private var query + + func update(from world: World) { + query.update(from: world) + } + + func execute(context: inout Context, renderContext: RenderContext) async -> [RenderSlotValue] { + query.forEach { (entity, camera) in + guard camera.isActive else { return } - await context.runSubgraph(by: Scene2DPlugin.renderGraph, inputs: [ + context.runSubgraph(by: Scene2DPlugin.renderGraph, inputs: [ RenderSlotValue(name: Scene2DPlugin.InputNode.view, value: .entity(entity)) ], viewEntity: entity) } diff --git a/Sources/AdaRender/CameraSystem.swift b/Sources/AdaRender/CameraSystem.swift index 31edb506c..41979d10a 100644 --- a/Sources/AdaRender/CameraSystem.swift +++ b/Sources/AdaRender/CameraSystem.swift @@ -9,29 +9,29 @@ import AdaTransform import AdaUtils import Math +import AdaAssets // FIXME: Currently we render on window directly // TODO: Move window info to ECS system /// System for updating cameras data on scene. -@System +@PlainSystem public struct CameraSystem: Sendable { @Query, GlobalTransform> private var query + @Res + private var primaryWindow: PrimaryWindowId + public init(world: World) { } - public func update(context: inout UpdateContext) { + @MainActor + public func update(context: UpdateContext) { self.query.forEach { entity, camera, globalTransform in let viewMatrix = globalTransform.matrix.inverse camera.viewMatrix = viewMatrix - - self.updateViewportSizeIfNeeded( - for: &camera.wrappedValue, - screenScale: 2, - windowSize: Size(width: 800, height: 600) // FIXME: Must use actual size - ) + self.updateViewportSizeIfNeeded(for: camera) self.updateProjectionMatrix(for: &camera.wrappedValue) self.updateFrustum(for: &camera.wrappedValue) @@ -43,26 +43,32 @@ public struct CameraSystem: Sendable { } } + @MainActor private func updateViewportSizeIfNeeded( - for camera: inout Camera, - screenScale: Float, - windowSize: Size + for camera: Ref ) { switch camera.renderTarget { case .window(let windowRef): camera.renderTarget = .window(windowRef) - camera.computedData.targetScaleFactor = screenScale + + guard let renderWindow = RenderEngine.shared + .getRenderWindow(for: windowRef.getWindowId(from: primaryWindow)) + else { + return + } + + camera.computedData.targetScaleFactor = renderWindow.scaleFactor if camera.viewport == nil { - camera.viewport = Viewport(rect: Rect(origin: .zero, size: windowSize)) + camera.viewport = Viewport(rect: Rect(origin: .zero, size: renderWindow.physicalSize)) return } - if camera.viewport?.rect.size != windowSize { - camera.viewport?.rect.size = windowSize + if camera.viewport?.rect.size != renderWindow.physicalSize { + camera.viewport?.rect.size = renderWindow.physicalSize } case .texture(let textureHandle): - let texture = textureHandle.asset + let texture = textureHandle.asset! let size = Size(width: Float(texture.width), height: Float(texture.height)) if camera.viewport == nil { @@ -117,33 +123,32 @@ public struct CameraSystem: Sendable { } @System -public struct ExtractCameraSystem { - - @Extract> - private var query - - public init(world: World) { } - - public func update(context: inout UpdateContext) { - self.query.wrappedValue.forEach { entity, camera, _, _ in - let cameraEntity = Entity(name: "ExtractedCameraEntity") - if - let bufferSet = entity.components[GlobalViewUniformBufferSet.self], - let uniform = entity.components[GlobalViewUniform.self] - { - let buffer = bufferSet.uniformBufferSet.getBuffer( - binding: GlobalBufferIndex.viewUniform, - set: 0, - frameIndex: RenderEngine.shared.currentFrameIndex - ) - - buffer.setData(uniform) - } - - cameraEntity.components = entity.components - cameraEntity.components += RenderItems() - cameraEntity.components.entity = cameraEntity - context.world.addEntity(cameraEntity) +@inline(__always) +public func ExtractCamera( + _ world: World, + _ commands: Commands, + _ query: Extract< + Query + > +) { + query.wrappedValue.forEach { + camera, transform, + visibleEntities, bufferSet, uniform in + let buffer = bufferSet.uniformBufferSet.getBuffer( + binding: GlobalBufferIndex.viewUniform, + set: 0, + frameIndex: RenderEngine.shared.currentFrameIndex + ) + + buffer.setData(uniform) + commands.spawn("ExtractedCameraEntity") { + camera + transform + visibleEntities + uniform + bufferSet + RenderViewTarget() + RenderItems() } } } diff --git a/Sources/AdaRender/CorePipelines/RenderNodes2D/Main2DRenderNode.swift b/Sources/AdaRender/CorePipelines/RenderNodes2D/Main2DRenderNode.swift new file mode 100644 index 000000000..5a773597a --- /dev/null +++ b/Sources/AdaRender/CorePipelines/RenderNodes2D/Main2DRenderNode.swift @@ -0,0 +1,126 @@ +// +// Main2DRenderNode.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 26.11.2025. +// + +import AdaECS +import AdaUtils + +/// This render node responsible for rendering ``Transparent2DRenderItem``. +public struct Main2DRenderNode: RenderNode { + + /// Input slots of render node. + public enum InputNode { + public static let view = "view" + } + + @Query< + Entity, + Camera, + RenderItems, + RenderViewTarget + > + private var query + + public init() {} + + public let inputResources: [RenderSlot] = [ + RenderSlot(name: InputNode.view, kind: .entity) + ] + + public func update(from world: World) { + query.update(from: world) + } + + public func execute(context: inout Context, renderContext: RenderContext) async throws -> [RenderSlotValue] { + guard let view = context.viewEntity else { + return [] + } + + try query.forEach { entity, camera, renderItems, target in + if entity != view { + return + } + + let sortedRenderItems = renderItems.sorted() + let clearColor = camera.clearFlags.contains(.solid) ? camera.backgroundColor : .surfaceClearColor + let commandBuffer = renderContext.commandQueue.makeCommandBuffer() + + guard + let texture = target.mainTexture + else { + return + } + + let renderPass = commandBuffer.beginRenderPass( + RenderPassDescriptor( + label: "Main 2d Render Pass", + colorAttachments: [ + .init( + texture: texture, + operation: OperationDescriptor( + loadAction: .clear, + storeAction: .store + ), + clearColor: clearColor + ) + ], + depthStencilAttachment: nil + ) + ) + + if let viewport = camera.viewport { + renderPass.setViewport(viewport.rect) + } + + if !sortedRenderItems.items.isEmpty { + try sortedRenderItems.render(with: renderPass, world: context.world, view: view) + } + + renderPass.endRenderPass() + commandBuffer.commit() + } + + return [] + } +} + +/// An object describe 2D render item. +public struct Transparent2DRenderItem: RenderItem { + + /// An entity that hold additional information about render item. + public var entity: Entity.ID + + /// An entity for batch rendering. + public var batchEntity: Entity.ID + + /// Draw pass which will be used for rendering this item. + public var drawPass: any DrawPass + + /// Render Pipeline for rendering this item. + public var renderPipeline: RenderPipeline + + /// Sort key used for rendering order. + public var sortKey: Float + + /// If item support batch rendering, pass range of indecies. + public var batchRange: Range? + + public init( + entity: Entity.ID, + batchEntity: Entity.ID, + drawPass: any DrawPass, + renderPipeline: RenderPipeline, + sortKey: Float, + batchRange: Range? = nil + ) { + self.entity = entity + self.batchEntity = batchEntity + self.drawPass = drawPass + self.renderPipeline = renderPipeline + self.sortKey = sortKey + self.batchRange = batchRange + } +} diff --git a/Sources/AdaRender/CorePipelines/RenderNodes2D/UpscaleNode.swift b/Sources/AdaRender/CorePipelines/RenderNodes2D/UpscaleNode.swift new file mode 100644 index 000000000..50c79ed8e --- /dev/null +++ b/Sources/AdaRender/CorePipelines/RenderNodes2D/UpscaleNode.swift @@ -0,0 +1,77 @@ +// +// UpscaleNode.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 26.11.2025. +// + +import AdaECS +import Math + +/// This node is responsible for presenting the result to the screen. +public struct UpscaleNode: RenderNode { + + public enum InputNode { + public static let view = "view" + } + + public let inputResources: [RenderSlot] = [ + RenderSlot(name: InputNode.view, kind: .entity) + ] + + public init() {} + + public func execute( + context: inout Context, + renderContext: RenderContext + ) async throws -> [RenderSlotValue] { + guard + let viewEntity = context.viewEntity, + let target = viewEntity.components[RenderViewTarget.self], + let camera = viewEntity.components[Camera.self] + else { + return [] + } + + guard let upsalePipeline = context.world.getResource(UpscalePipeline.self) else { + return [] + } + + if let mainTexture = target.mainTexture, + let outputTexture = target.outputTexture, + mainTexture !== outputTexture { + let commandBuffer = renderContext.commandQueue.makeCommandBuffer() + let renderPass = commandBuffer.beginRenderPass( + RenderPassDescriptor( + label: "Upscale Pass", + colorAttachments: [ + .init( + texture: outputTexture, + operation: OperationDescriptor( + loadAction: .clear, + storeAction: .store + ), + clearColor: camera.backgroundColor + ) + ], + depthStencilAttachment: nil + ) + ) + + // This code doesn't work + if let viewport = camera.viewport, Int(viewport.rect.width) == outputTexture.width && Int(viewport.rect.height) == outputTexture.height { + renderPass.setScissorRect(viewport.rect) + } + + renderPass.setFragmentTexture(mainTexture, index: 0) + renderPass.setFragmentSamplerState(upsalePipeline.sampler, index: 0) + renderPass.setRenderPipelineState(upsalePipeline.renderPipeline) + + renderPass.draw(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1) + renderPass.endRenderPass() + commandBuffer.commit() + } + + return [] + } +} diff --git a/Sources/AdaRender/CorePipelines/Scene2DPlugin.swift b/Sources/AdaRender/CorePipelines/Scene2DPlugin.swift new file mode 100644 index 000000000..c0bfe02f9 --- /dev/null +++ b/Sources/AdaRender/CorePipelines/Scene2DPlugin.swift @@ -0,0 +1,57 @@ +// +// Scene2DPlugin.swift +// AdaEngine +// +// Created by v.prusakov on 2/19/23. +// + +import AdaApp +import AdaECS +import AdaUtils +import Math + +/// Plugin for RenderWorld added 2D render capatibilites. +public struct Scene2DPlugin: Plugin { + + /// Render graph name. + public static let renderGraph = "render_graph_2d" + + public init() {} + + /// Input slots of render graph. + public enum InputNode { + public static let view = "view" + } + + public func setup(in app: AppWorlds) { + guard let app = app.getSubworldBuilder(by: .renderWorld) else { + return + } + + // Add Systems + app.addSystem(BatchTransparent2DItemsSystem.self) + + Task { @RenderGraphActor in + var graph = RenderGraph(label: "Scene 2D Render Graph") + let entryNode = graph.addEntryNode(inputs: [ + RenderSlot(name: InputNode.view, kind: .entity) + ]) + + graph.addNode(Main2DRenderNode()) + graph.addNode(UpscaleNode()) + + graph.addSlotEdge( + fromNode: entryNode, + outputSlot: InputNode.view, + toNode: Main2DRenderNode.name, + inputSlot: Main2DRenderNode.InputNode.view + ) + graph.addNodeEdge(from: Main2DRenderNode.name, to: UpscaleNode.name) + + await app + .getRefResource(RenderGraph.self) + .wrappedValue + .addSubgraph(graph, name: Self.renderGraph) + } + } +} diff --git a/Sources/AdaRender/CorePipelines/Upscaling.swift b/Sources/AdaRender/CorePipelines/Upscaling.swift new file mode 100644 index 000000000..7519f65b0 --- /dev/null +++ b/Sources/AdaRender/CorePipelines/Upscaling.swift @@ -0,0 +1,52 @@ +// +// Upscaling.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 26.11.2025. +// + +import AdaApp +import AdaAssets +import AdaECS +import AdaUtils +import Math + +struct UpscalePlugin: Plugin { + func setup(in app: borrowing AdaApp.AppWorlds) { + guard let renderWorld = app.getSubworldBuilder(by: .renderWorld) else { + return + } + let renderDevice = renderWorld.getResource(RenderDeviceHandler.self) + .unwrap(message: "Failed to fetch RenderDevice from world") + .renderDevice + + renderWorld + .insertResource(UpscalePipeline(device: renderDevice)) + } +} + +public struct UpscalePipeline: Resource { + + public let renderPipeline: RenderPipeline + public let sampler: Sampler + + public init(device: RenderDevice) { + let spriteShader = try! AssetsManager.loadSync( + ShaderModule.self, + at: "Shaders/FullScreenShader.glsl", + from: .module + ) + + var descriptor = RenderPipelineDescriptor() + descriptor.debugName = "Upscale Pipeline" + + descriptor.vertex = spriteShader.asset.getShader(for: .vertex) + descriptor.fragment = spriteShader.asset.getShader(for: .fragment) + descriptor.colorAttachments = [ + RenderPipelineColorAttachmentDescriptor(format: .bgra8) + ] + + self.renderPipeline = device.createRenderPipeline(from: descriptor) + self.sampler = device.createSampler(from: SamplerDescriptor()) + } +} diff --git a/Sources/AdaRender/Extract.swift b/Sources/AdaRender/Extract.swift new file mode 100644 index 000000000..56e8ee638 --- /dev/null +++ b/Sources/AdaRender/Extract.swift @@ -0,0 +1,43 @@ +// +// Extract.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 25.11.2025. +// + +import AdaECS + +/// A property wrapper that allows you to extract a resource from the main world. +@propertyWrapper +public final class Extract: @unchecked Sendable { + private var _value: T! + public var wrappedValue: T { + _read { yield self._value } + } + + /// Initialize a new extract. + public init() { } + + /// Initialize a new extract. + /// - Parameter from: The world to extract the resource from. + public init(from world: World) { + self._value = T.init(from: world) + } + + /// Call the extract. + /// - Returns: The extracted resource. + public func callAsFunction() -> T { + self._value + } +} + +extension Extract: SystemParameter { + public func update(from world: World) { + if let mainWorld = world.getResource(MainWorld.self)?.world { + if _value == nil { + _value = T.init(from: mainWorld) + } + _value?.update(from: mainWorld) + } + } +} diff --git a/Sources/AdaRender/Image/Image.swift b/Sources/AdaRender/Image/Image.swift index f473d3180..2ab1f46c8 100644 --- a/Sources/AdaRender/Image/Image.swift +++ b/Sources/AdaRender/Image/Image.swift @@ -7,17 +7,24 @@ import AdaAssets import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Math /// An object that manages image data in your app. -public final class Image: @unchecked Sendable { - +public struct Image: Sendable { + public private(set) var data: Data public private(set) var height: Int public private(set) var width: Int - + + @_spi(Internal) + public var options: [String: any Sendable] = [:] + // TODO: Replace it to PixelFormat public private(set) var format: Format @@ -58,7 +65,7 @@ public final class Image: @unchecked Sendable { } /// Set pixel color for specific X and Y position. - public func setPixel(in position: Point, color: Color) { + public mutating func setPixel(in position: Point, color: Color) { let offset = Int(position.y) * self.width + Int(position.x) Self.setPixel(with: offset, color: color, in: &self.data, format: self.format) @@ -92,7 +99,7 @@ public final class Image: @unchecked Sendable { } public extension Image { - enum Format: UInt16, Codable { + enum Format: UInt16, Codable, Sendable { case rgba8 case rgb8 case bgra8 @@ -121,7 +128,7 @@ public extension Image { PNGImageSerializer() ] - convenience init(contentsOf file: URL) throws { + init(contentsOf file: URL) throws { guard let loader = Self.loaders.first(where: { $0.canDecodeImage(with: file.pathExtension) }) else { throw LoadingError.formatNotSupported(file.pathExtension) } @@ -150,11 +157,11 @@ extension Image: Asset { let sampler: SamplerDescriptor } - public convenience init(from assetDecoder: AssetDecoder) throws { + public init(from assetDecoder: AssetDecoder) async throws { let pathExt = assetDecoder.assetMeta.filePath.pathExtension if pathExt.isEmpty || pathExt == "res" { - let rep = try assetDecoder.decode(ImageRepresentation.self) + let rep = try await assetDecoder.decode(ImageRepresentation.self) self.init( width: Int(rep.imageSize.width), @@ -189,13 +196,6 @@ extension Image: Asset { public static func extensions() -> [String] { ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp"] } - - public func update(_ newImage: Image) async throws { - self.data = newImage.data - self.width = newImage.width - self.height = newImage.height - self.format = newImage.format - } } private extension Image { diff --git a/Sources/AdaRender/Image/ImageLoaders/ImageLoaderStrategy.swift b/Sources/AdaRender/Image/ImageLoaders/ImageLoaderStrategy.swift index 4b1497562..51378eac4 100644 --- a/Sources/AdaRender/Image/ImageLoaders/ImageLoaderStrategy.swift +++ b/Sources/AdaRender/Image/ImageLoaders/ImageLoaderStrategy.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 6/28/22. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// An interface that describe how to build an ``Image`` object from bytes. protocol ImageLoaderStrategy: Sendable { diff --git a/Sources/AdaRender/Image/ImageLoaders/PNGSerializer.swift b/Sources/AdaRender/Image/ImageLoaders/PNGSerializer.swift index 3ca28cecc..4e5d5b1f7 100644 --- a/Sources/AdaRender/Image/ImageLoaders/PNGSerializer.swift +++ b/Sources/AdaRender/Image/ImageLoaders/PNGSerializer.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 6/29/22. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import libpng /// An object that serialize png raw data to an ``Image`` @@ -24,11 +28,11 @@ struct PNGImageSerializer: ImageLoaderStrategy { } func decodeImage(from data: Data) throws -> Image { - var pngImage = png_image() - pngImage.version = png_uint_32(PNG_IMAGE_VERSION) - - var isSuccess = data.withUnsafeBytes { bufferPtr in - return png_image_begin_read_from_memory(&pngImage, bufferPtr.baseAddress, data.count) == 1 + var pngImage = unsafe png_image() + unsafe pngImage.version = png_uint_32(PNG_IMAGE_VERSION) + + var isSuccess = unsafe data.withUnsafeBytes { bufferPtr in + return unsafe png_image_begin_read_from_memory(&pngImage, bufferPtr.baseAddress, data.count) == 1 } let maskFormat: UInt32 = ~( @@ -36,12 +40,12 @@ struct PNGImageSerializer: ImageLoaderStrategy { PNG_FORMAT_FLAG_LINEAR | PNG_FORMAT_FLAG_COLORMAP ) - pngImage.format &= maskFormat - pngImage.flags |= UInt32(PNG_IMAGE_FLAG_16BIT_sRGB) - + unsafe pngImage.format &= maskFormat + unsafe pngImage.flags |= UInt32(PNG_IMAGE_FLAG_16BIT_sRGB) + let format: Image.Format - switch pngImage.format { + switch unsafe pngImage.format { case PNG_FORMAT_FLAG_COLOR: format = .rgb8 case (PNG_FORMAT_FLAG_COLOR | PNG_FORMAT_FLAG_ALPHA): // rgba @@ -51,27 +55,27 @@ struct PNGImageSerializer: ImageLoaderStrategy { case UInt32(PNG_FORMAT_GRAY): format = .gray default: - png_image_free(&pngImage) + unsafe png_image_free(&pngImage) throw DecodingError.notSupportedImageFormat } if !isSuccess { - png_image_free(&pngImage) + unsafe png_image_free(&pngImage) throw DecodingError.cannotReadFromMemmory } - let stride = swift_png_image_row_stride(pngImage) - var imageBuffer = Data(count: Int(swift_png_image_buffer_size(pngImage, stride))) - - isSuccess = imageBuffer.withUnsafeMutableBytes { - png_image_finish_read(&pngImage, nil, $0.baseAddress, png_int_32(stride), nil) == 1 + let stride = unsafe swift_png_image_row_stride(pngImage) + var imageBuffer = unsafe Data(count: Int(swift_png_image_buffer_size(pngImage, stride))) + + isSuccess = unsafe imageBuffer.withUnsafeMutableBytes { + unsafe png_image_finish_read(&pngImage, nil, $0.baseAddress, png_int_32(stride), nil) == 1 } if !isSuccess { throw DecodingError.cannotFinishReading } - return Image( + return unsafe Image( width: Int(pngImage.width), height: Int(pngImage.height), data: imageBuffer, diff --git a/Sources/AdaRender/Materials/Material.swift b/Sources/AdaRender/Materials/Material.swift index 8cdf06459..b31c03a75 100644 --- a/Sources/AdaRender/Materials/Material.swift +++ b/Sources/AdaRender/Materials/Material.swift @@ -44,22 +44,22 @@ public class Material: Asset, Hashable, @unchecked Sendable { /// Set the new value for material. public func setValue(_ value: T, for name: String) { - MaterialStorage.shared.setValue(value, for: name, in: self) + unsafe MaterialStorage.shared.setValue(value, for: name, in: self) } /// Get value from material. public func getValue(for name: String) -> T? { - return MaterialStorage.shared.getValue(for: name, in: self) + return unsafe MaterialStorage.shared.getValue(for: name, in: self) } /// Set one or more textures for material. public func setResources(_ textures: [Texture], for name: String) { - MaterialStorage.shared.setResources(textures, for: name, in: self) + unsafe MaterialStorage.shared.setResources(textures, for: name, in: self) } /// Get textures from material. public func getResources(for name: String) -> [Texture] { - return MaterialStorage.shared.getResources(for: name, in: self) + return unsafe MaterialStorage.shared.getResources(for: name, in: self) } /// Updates material values. @@ -85,7 +85,11 @@ public class Material: Asset, Hashable, @unchecked Sendable { } /// Create render pipeline descriptor for passed vertex descriptor, keys and compiled shader module. - open func configureRenderPipeline(for vertexDescriptor: VertexDescriptor, keys: Set, shaderModule: ShaderModule) -> RenderPipelineDescriptor? { + open func configureRenderPipeline( + for vertexDescriptor: VertexDescriptor, + keys: Set, + shaderModule: ShaderModule + ) -> RenderPipelineDescriptor? { fatalErrorMethodNotImplemented() } } diff --git a/Sources/AdaRender/Materials/MaterialStorage.swift b/Sources/AdaRender/Materials/MaterialStorage.swift index 89d69b61a..1669515bd 100644 --- a/Sources/AdaRender/Materials/MaterialStorage.swift +++ b/Sources/AdaRender/Materials/MaterialStorage.swift @@ -8,6 +8,9 @@ import AdaAssets import AdaUtils +// TODO: I don't like that solution +// - Remove or replace RenderEngine +// - Should be a class or be open for inheritance? open class MaterialStorageData { public var reflectionData: ShaderReflectionData = ShaderReflectionData() public var uniformBufferSet: [String : UniformBufferSet] = [:] @@ -40,9 +43,7 @@ open class MaterialStorageData { } public final class MaterialStorage { - public nonisolated(unsafe) static let shared: MaterialStorage = MaterialStorage() - private var materialData: [RID: MaterialStorageData] = [:] private init() {} @@ -66,9 +67,9 @@ public final class MaterialStorage { frameIndex: RenderEngine.shared.currentFrameIndex ) - withUnsafePointer(to: value) { pointer in - let dataPtr = UnsafeMutableRawPointer(mutating: UnsafeRawPointer(pointer)) - buffer?.setData(dataPtr, byteCount: member.size, offset: member.offset) + unsafe withUnsafePointer(to: value) { pointer in + let dataPtr = unsafe UnsafeMutableRawPointer(mutating: UnsafeRawPointer(pointer)) + unsafe buffer?.setData(dataPtr, byteCount: member.size, offset: member.offset) } } @@ -89,7 +90,7 @@ public final class MaterialStorage { frameIndex: RenderEngine.shared.currentFrameIndex ) - return buffer?.contents().load(fromByteOffset: member.offset, as: T.self) + return unsafe buffer?.contents().load(fromByteOffset: member.offset, as: T.self) } public func getUniformDescription(for name: String, in material: MaterialStorageData) -> ShaderResource.ShaderBuffer? { diff --git a/Sources/AdaRender/Materials/ReflectedMaterial/CanvasMaterial.swift b/Sources/AdaRender/Materials/ReflectedMaterial/CanvasMaterial.swift index 3a4da5f92..986c94740 100644 --- a/Sources/AdaRender/Materials/ReflectedMaterial/CanvasMaterial.swift +++ b/Sources/AdaRender/Materials/ReflectedMaterial/CanvasMaterial.swift @@ -67,7 +67,7 @@ public extension CanvasMaterial { descriptor.vertexDescriptor = vertexDescriptor descriptor.backfaceCulling = true descriptor.colorAttachments = [ - ColorAttachmentDescriptor( + RenderPipelineColorAttachmentDescriptor( format: .bgra8, isBlendingEnabled: true ) diff --git a/Sources/AdaRender/Materials/ReflectedMaterial/CustomMaterial.swift b/Sources/AdaRender/Materials/ReflectedMaterial/CustomMaterial.swift index 052d05425..4abe1b617 100644 --- a/Sources/AdaRender/Materials/ReflectedMaterial/CustomMaterial.swift +++ b/Sources/AdaRender/Materials/ReflectedMaterial/CustomMaterial.swift @@ -7,6 +7,11 @@ import AdaAssets import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif protocol MaterialValueDelegate: AnyObject { func updateValue(_ value: ShaderUniformValue, for name: String) diff --git a/Sources/AdaRender/Mesh/MeshArray.swift b/Sources/AdaRender/Mesh/MeshArray.swift index b8404e5aa..ed73b03ee 100644 --- a/Sources/AdaRender/Mesh/MeshArray.swift +++ b/Sources/AdaRender/Mesh/MeshArray.swift @@ -73,24 +73,25 @@ public struct MeshBuffer: Sequence { } extension MeshBuffer { - + + @safe public struct ChunkIterator: IteratorProtocol { private let buffer: _MeshBuffer private let currentChunk: UnsafeMutablePointer internal init(buffer: _MeshBuffer) { self.buffer = buffer - self.currentChunk = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size) - self.currentChunk.pointee = 0 + unsafe self.currentChunk = UnsafeMutablePointer.allocate(capacity: MemoryLayout.size) + unsafe self.currentChunk.pointee = 0 } public func next() -> T? { - let nextElement = self.buffer.getChunk(withOffset: currentChunk.pointee, type: T.self) - currentChunk.pointee += MemoryLayout.stride - + let nextElement = unsafe self.buffer.getChunk(withOffset: currentChunk.pointee, type: T.self) + unsafe currentChunk.pointee += MemoryLayout.stride + if nextElement == nil { - currentChunk.deinitialize(count: 1) - currentChunk.deallocate() + unsafe currentChunk.deinitialize(count: 1) + unsafe currentChunk.deallocate() return nil } @@ -138,6 +139,7 @@ extension MeshBuffer: ExpressibleByArrayLiteral { } } +@safe class _MeshBuffer: Equatable, @unchecked Sendable { internal let bytes: UnsafeMutableRawBufferPointer @@ -155,33 +157,33 @@ class _MeshBuffer: Equatable, @unchecked Sendable { alignment: MemoryLayout.alignment ) - withUnsafePointer(to: elements) { pointer in - bytes.baseAddress?.copyMemory( + unsafe withUnsafePointer(to: elements) { pointer in + unsafe bytes.baseAddress?.copyMemory( from: pointer, byteCount: elementSize * elements.count ) } - self.bytes = bytes - self.indicesPointer = UnsafeMutableBufferPointer.allocate(capacity: indices.count) - _ = self.indicesPointer.initialize(from: indices) + unsafe self.bytes = bytes + unsafe self.indicesPointer = UnsafeMutableBufferPointer.allocate(capacity: indices.count) + _ = unsafe self.indicesPointer.initialize(from: indices) } deinit { - self.bytes.deallocate() - self.indicesPointer.deallocate() + unsafe self.bytes.deallocate() + unsafe self.indicesPointer.deallocate() } // MARK: - Internal static func == (lhs: _MeshBuffer, rhs: _MeshBuffer) -> Bool { - lhs.bytes.elementsEqual(rhs.bytes) && + unsafe lhs.bytes.elementsEqual(rhs.bytes) && lhs.indicesPointer.elementsEqual(rhs.indicesPointer) && lhs.elementSize == rhs.elementSize } var count: Int { - return self.bytes.count / self.elementSize + return unsafe self.bytes.count / self.elementSize } func iterateByElements(_ block: (Int, UnsafeMutableRawPointer) -> Void) { @@ -189,27 +191,27 @@ class _MeshBuffer: Equatable, @unchecked Sendable { let count = self.count while currentIndex < count { - let pointer = self.bytes.baseAddress!.advanced(by: currentIndex * self.elementSize) - - block(currentIndex, pointer) - + let pointer = unsafe self.bytes.baseAddress!.advanced(by: currentIndex * self.elementSize) + + unsafe block(currentIndex, pointer) + currentIndex += 1 } } func getChunk(withOffset offset: Int, type: T.Type) -> T? { - guard offset < self.bytes.endIndex else { + guard unsafe offset < self.bytes.endIndex else { return nil } - return self.bytes.load(fromByteOffset: offset, as: T.self) + return unsafe self.bytes.load(fromByteOffset: offset, as: T.self) } func getIndices() -> [UInt32] { - Array(self.indicesPointer) + unsafe Array(self.indicesPointer) } func getData() -> [Element] { - Array(self.bytes.bindMemory(to: Element.self)) + unsafe Array(self.bytes.bindMemory(to: Element.self)) } } diff --git a/Sources/AdaRender/Mesh/MeshDescriptor.swift b/Sources/AdaRender/Mesh/MeshDescriptor.swift index 9ab3c2f6c..b5603bee7 100644 --- a/Sources/AdaRender/Mesh/MeshDescriptor.swift +++ b/Sources/AdaRender/Mesh/MeshDescriptor.swift @@ -238,7 +238,7 @@ public extension MeshDescriptor { func getIndexBuffer() -> IndexBuffer { var indicies = self.indicies - let indexBuffer = RenderEngine.shared.renderDevice.createIndexBuffer( + let indexBuffer = unsafe RenderEngine.shared.renderDevice.createIndexBuffer( format: .uInt32, bytes: &indicies, length: indicies.count * MemoryLayout.size @@ -261,9 +261,9 @@ public extension MeshDescriptor { for buffer in buffers.elements.values { let elementSize = buffer.buffer.elementSize - buffer.buffer.iterateByElements { index, pointer in + unsafe buffer.buffer.iterateByElements { index, pointer in let offset = index * vertexSize + attributeOffset - vertexBufferContents + unsafe vertexBufferContents .advanced(by: offset) .copyMemory(from: pointer, byteCount: elementSize) } diff --git a/Sources/AdaRender/Misc/Buffer.swift b/Sources/AdaRender/Misc/Buffer.swift index ac5372790..ad307e131 100644 --- a/Sources/AdaRender/Misc/Buffer.swift +++ b/Sources/AdaRender/Misc/Buffer.swift @@ -32,7 +32,7 @@ public extension Buffer { /// - Parameter bytes: A pointer to the data which will be copied. /// - Parameter byteCount: Count of bytes which will be copied. func setData(_ bytes: UnsafeMutableRawPointer, byteCount: Int) { - self.setData(bytes, byteCount: byteCount, offset: 0) + unsafe self.setData(bytes, byteCount: byteCount, offset: 0) } /// Set data to the buffer's storage. @@ -40,8 +40,16 @@ public extension Buffer { func setData(_ value: T) { let size = MemoryLayout.stride - withUnsafePointer(to: value) { ptr in - self.setData(UnsafeMutableRawPointer(mutating: ptr), byteCount: size) + unsafe withUnsafePointer(to: value) { ptr in + unsafe self.setData(UnsafeMutableRawPointer(mutating: ptr), byteCount: size) + } + } + + /// Set elements to the buffer's storage. + /// - Parameter value: A value which will be copied. + func setElements(_ elements: inout [T]) { + unsafe elements.withUnsafeMutableBytes { ptr in + unsafe self.setData(ptr.baseAddress!, byteCount: ptr.count) } } } diff --git a/Sources/AdaRender/Misc/BufferData.swift b/Sources/AdaRender/Misc/BufferData.swift new file mode 100644 index 000000000..0320a5aa7 --- /dev/null +++ b/Sources/AdaRender/Misc/BufferData.swift @@ -0,0 +1,109 @@ +// +// BufferData.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 23.11.2025. +// + +public struct BufferData { + public var elements: [T] { + didSet { + self.isChanged = true + } + } + public var buffer: (any Buffer)? + public var label: String? + public var isChanged: Bool = false + + public init(label: String? = nil, elements: [T]) { + self.label = label + self.elements = elements + } +} + +extension BufferData: Equatable where T: Equatable { + public static func == (lhs: BufferData, rhs: BufferData) -> Bool { + lhs.elements == rhs.elements + } +} + +extension BufferData: Hashable where T: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.elements) + } +} + +extension BufferData: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: T...) { + self.label = nil + self.elements = elements + } +} + +extension BufferData: RandomAccessCollection { + public typealias Element = T + public typealias Index = Int + + public subscript(position: Int) -> T { + _read { + yield elements[position] + } + _modify { + yield &elements[position] + } + } + + public var startIndex: Int { + elements.startIndex + } + + public var endIndex: Int { + elements.endIndex + } +} + +public extension BufferData { + + var isEmpty: Bool { + self.elements.isEmpty + } + + var count: Int { + self.elements.count + } + + var bufferLength: Int { + self.buffer?.length ?? 0 + } + + mutating func write(to renderDevice: RenderDevice) { + reserveCapacity(self.elements.count, for: renderDevice) + guard let buffer else { + return + } + buffer.setElements(&elements) + } + + mutating func reserveCapacity(_ count: Int, for renderDevice: RenderDevice) { + let newCapacity = MemoryLayout.stride * count + if bufferLength >= newCapacity { + return + } + self.buffer = renderDevice.createBuffer(label: label, length: newCapacity, options: .storageShared) + self.isChanged = false + } + + mutating func append(_ element: T) { + self.elements.append(element) + self.isChanged = true + } + + mutating func removeAll() { + self.elements.removeAll() + } +} + +extension BufferData: Sequence { } + +extension BufferData: Sendable where T: Sendable { } + diff --git a/Sources/AdaRender/Misc/DrawList.swift b/Sources/AdaRender/Misc/DrawList.swift index 2b4155026..58dcfeb9e 100644 --- a/Sources/AdaRender/Misc/DrawList.swift +++ b/Sources/AdaRender/Misc/DrawList.swift @@ -134,11 +134,11 @@ public final class DrawList: @unchecked Sendable { indexBufferOffset: Int = 0, instanceCount: Int ) { - renderDevice.draw( - self, - indexCount: indexCount, - indexBufferOffset: indexBufferOffset, - instanceCount: instanceCount - ) +// renderDevice.draw( +// self, +// indexCount: indexCount, +// indexBufferOffset: indexBufferOffset, +// instanceCount: instanceCount +// ) } } diff --git a/Sources/AdaRender/Misc/RenderPassDescriptor.swift b/Sources/AdaRender/Misc/RenderPassDescriptor.swift new file mode 100644 index 000000000..f4fc20208 --- /dev/null +++ b/Sources/AdaRender/Misc/RenderPassDescriptor.swift @@ -0,0 +1,50 @@ +// +// RenderPassDescriptor.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 26.11.2025. +// + +import AdaUtils +import Math + +public struct RenderPassDescriptor: Sendable { + + public var label: String? + + public var colorAttachments: [RenderPassColorAttachmentDescriptor] + + public var depthStencilAttachment: DepthStencilAttachmentDescriptor? + + public init( + label: String? = nil, + colorAttachments: [RenderPassColorAttachmentDescriptor], + depthStencilAttachment: DepthStencilAttachmentDescriptor? = nil + ) { + self.colorAttachments = colorAttachments + self.depthStencilAttachment = depthStencilAttachment + } +} + +public struct RenderPassColorAttachmentDescriptor: Sendable { + + public var texture: Texture + + public var resolveTexture: Texture? + + public var operation: OperationDescriptor? + + public var clearColor: Color? + + public init( + texture: Texture, + resolveTexture: Texture? = nil, + operation: OperationDescriptor? = nil, + clearColor: Color? = nil + ) { + self.texture = texture + self.resolveTexture = resolveTexture + self.operation = operation + self.clearColor = clearColor + } +} diff --git a/Sources/AdaRender/Misc/RenderPipeline.swift b/Sources/AdaRender/Misc/RenderPipeline.swift index 1ead9daea..edf9a3661 100644 --- a/Sources/AdaRender/Misc/RenderPipeline.swift +++ b/Sources/AdaRender/Misc/RenderPipeline.swift @@ -5,6 +5,9 @@ // Created by v.prusakov on 1/8/23. // +import AdaUtils +import Math + // TODO: (Vlad) Add documentations /// An object that contains graphics functions and configuration state to use in a render command. @@ -113,7 +116,7 @@ public struct DepthStencilDescriptor: Sendable { } /// An object that specifies the format and properties of a color attachment. -public struct ColorAttachmentDescriptor: Sendable { +public struct RenderPipelineColorAttachmentDescriptor: Sendable { /// The format of the color attachment. public var format: PixelFormat @@ -166,6 +169,40 @@ public struct ColorAttachmentDescriptor: Sendable { } } +public struct DepthStencilAttachmentDescriptor: Sendable { + + public var texture: Texture + + public var depthOperation: OperationDescriptor? + + public var stencilOperation: OperationDescriptor? + + public init( + texture: Texture, + depthOperation: OperationDescriptor? = nil, + stencilOperation: OperationDescriptor? = nil + ) { + self.texture = texture + self.depthOperation = depthOperation + self.stencilOperation = stencilOperation + } +} + +public struct OperationDescriptor: Sendable { + + public var loadAction: AttachmentLoadAction + + public var storeAction: AttachmentStoreAction + + public init( + loadAction: AttachmentLoadAction, + storeAction: AttachmentStoreAction + ) { + self.loadAction = loadAction + self.storeAction = storeAction + } +} + /// An object specifies the rendering configuration state to use during a rendering pass, /// including rasterization (such as multisampling), visibility, blending, tessellation, and graphics function state. /// @@ -197,8 +234,8 @@ public struct RenderPipelineDescriptor: Sendable { public var depthPixelFormat: PixelFormat = .depth_32f_stencil8 /// The color attachments. - public var colorAttachments: [ColorAttachmentDescriptor] = [] - + public var colorAttachments: [RenderPipelineColorAttachmentDescriptor] = [] + /// Initialize a new render pipeline descriptor. /// /// - Parameter vertex: The vertex shader. @@ -214,7 +251,7 @@ public struct RenderPipelineDescriptor: Sendable { vertexDescriptor: VertexDescriptor = VertexDescriptor(), depthStencilDescriptor: DepthStencilDescriptor? = nil, depthPixelFormat: PixelFormat = .depth_32f_stencil8, - colorAttachments: [ColorAttachmentDescriptor] = [] + colorAttachments: [RenderPipelineColorAttachmentDescriptor] = [] ) { self.vertex = vertex self.fragment = fragment diff --git a/Sources/AdaRender/Misc/Sampler.swift b/Sources/AdaRender/Misc/Sampler.swift index 6ef62b2f8..216c59660 100644 --- a/Sources/AdaRender/Misc/Sampler.swift +++ b/Sources/AdaRender/Misc/Sampler.swift @@ -6,7 +6,7 @@ // /// Filtering options for determining which pixel value is returned within a mipmap level. -public enum SamplerMinMagFilter: String, Codable { +public enum SamplerMinMagFilter: String, Codable, Sendable { /// Select the single pixel nearest to the sample point. case nearest @@ -16,8 +16,8 @@ public enum SamplerMinMagFilter: String, Codable { } /// Filtering options for determining what pixel value is returned with multiple mipmap levels. -public enum SamplerMipFilter: String, Codable { - +public enum SamplerMipFilter: String, Codable, Sendable { + /// The nearest mipmap level is selected. case nearest @@ -29,8 +29,8 @@ public enum SamplerMipFilter: String, Codable { } /// An object that you use to configure a texture sampler. -public struct SamplerDescriptor: Codable { - +public struct SamplerDescriptor: Codable, Sendable { + /// The filtering option for combining pixels within one mipmap level when the sample footprint is larger than a pixel (minification). public var minFilter: SamplerMinMagFilter @@ -62,7 +62,7 @@ public struct SamplerDescriptor: Codable { } /// Sampler representation in GPU. You can create your own sampler instance for manage how to draw texture. -public protocol Sampler: AnyObject { +public protocol Sampler: AnyObject, Sendable { /// Contains information about sampler descriptor. var descriptor: SamplerDescriptor { get } diff --git a/Sources/AdaRender/Misc/TextureDescriptor.swift b/Sources/AdaRender/Misc/TextureDescriptor.swift index 37ccdb6aa..2886f1982 100644 --- a/Sources/AdaRender/Misc/TextureDescriptor.swift +++ b/Sources/AdaRender/Misc/TextureDescriptor.swift @@ -5,6 +5,8 @@ // Created by v.prusakov on 1/25/23. // +import Math + /// An object that you use to configure new texture objects. public struct TextureDescriptor { @@ -59,4 +61,6 @@ public struct TextureDescriptor { } /// Interface represent platform specific gpu texture. -public class GPUTexture { } +public protocol GPUTexture { + var size: SizeInt { get } +} diff --git a/Sources/AdaRender/Misc/VertexDescriptor.swift b/Sources/AdaRender/Misc/VertexDescriptor.swift index aa6cdbb79..8583eb380 100644 --- a/Sources/AdaRender/Misc/VertexDescriptor.swift +++ b/Sources/AdaRender/Misc/VertexDescriptor.swift @@ -220,7 +220,7 @@ extension VertexDescriptor: CustomStringConvertible { let newDesc = value.element.description + (shouldInsertColumn ? "," : "") return result + " " + newDesc + "\n" }) - return String(format: "VertexDescriptor: attributes(\n%@) layots: {\n%@}", attributesDesc, layoutsDesc) + return unsafe String(format: "VertexDescriptor: attributes(\n%@) layots: {\n%@}", attributesDesc, layoutsDesc) } } diff --git a/Sources/AdaRender/RenderEngine.swift b/Sources/AdaRender/RenderEngine.swift index 6b075ff13..683638e07 100644 --- a/Sources/AdaRender/RenderEngine.swift +++ b/Sources/AdaRender/RenderEngine.swift @@ -14,8 +14,8 @@ public enum GlobalBufferIndex { } /// Render Engine is object that manage a GPU. -public final class RenderEngine: RenderBackend { - +public final class RenderEngine: RenderBackend, Sendable { + public struct Configuration { public var maxFramesInFlight: Int = 3 public init() {} @@ -25,7 +25,7 @@ public final class RenderEngine: RenderBackend { nonisolated(unsafe) public static var configurations: Configuration = Configuration() /// Return instance of render engine for specific backend. - nonisolated(unsafe) public static let shared: RenderEngine = { + public static let shared: RenderEngine = { let renderBackend: RenderBackend let appName = "AdaEngine" @@ -68,26 +68,24 @@ public final class RenderEngine: RenderBackend { return self.renderBackend.createLocalRenderDevice() } - public func createWindow(_ windowRef: WindowRef, for surface: RenderSurface, size: SizeInt) throws { - try self.renderBackend.createWindow(windowRef, for: surface, size: size) + public func createWindow(_ windowId: WindowID, for surface: RenderSurface, size: SizeInt) throws { + try self.renderBackend.createWindow(windowId, for: surface, size: size) } - public func resizeWindow(_ windowRef: WindowRef, newSize: SizeInt) throws { - try self.renderBackend.resizeWindow(windowRef, newSize: newSize) + public func resizeWindow(_ windowId: WindowID, newSize: SizeInt) throws { + try self.renderBackend.resizeWindow(windowId, newSize: newSize) } - public func destroyWindow(_ windowRef: WindowRef) throws { - try self.renderBackend.destroyWindow(windowRef) + public func destroyWindow(_ windowId: WindowID) throws { + try self.renderBackend.destroyWindow(windowId) } - - public func beginFrame() throws { - preconditionMainThreadOnly() - try self.renderBackend.beginFrame() + + func getRenderWindow(for windowId: WindowID) -> RenderWindow? { + self.renderBackend.getRenderWindow(for: windowId) } - - public func endFrame() throws { - preconditionMainThreadOnly() - try self.renderBackend.endFrame() + + public func getRenderWindows() throws -> RenderWindows { + try self.renderBackend.getRenderWindows() } } diff --git a/Sources/AdaRender/RenderGraph/RenderGraph.swift b/Sources/AdaRender/RenderGraph/RenderGraph.swift index 3f8dfccb4..f1c54a1f5 100644 --- a/Sources/AdaRender/RenderGraph/RenderGraph.swift +++ b/Sources/AdaRender/RenderGraph/RenderGraph.swift @@ -6,10 +6,27 @@ // import AdaECS +import Logging // Inspired by Bevy https://github.com/bevyengine/bevy/tree/main/crates/bevy_render/src/render_graph -public struct RenderSlot { +public struct RenderContext: @unchecked Sendable { + public let device: RenderDevice + public let commandQueue: CommandQueue + public let commandEncoder: CommandBuffer? + + public init( + device: RenderDevice, + commandQueue: CommandQueue, + commandEncoder: CommandBuffer? = nil, + ) { + self.device = device + self.commandEncoder = commandEncoder + self.commandQueue = commandQueue + } +} + +public struct RenderSlot: Sendable { public let name: String public let kind: RenderResourceKind @@ -19,7 +36,7 @@ public struct RenderSlot { } } -public struct RenderSlotValue { +public struct RenderSlotValue: Sendable { public let name: String public let value: RenderResource @@ -33,7 +50,7 @@ public struct EmptyNode: RenderNode { public init() {} - public func execute(context: Context) -> [RenderSlotValue] { + public func execute(context: inout Context, renderContext: RenderContext) -> [RenderSlotValue] { return [] } } @@ -47,7 +64,7 @@ struct GraphEntryNode: RenderNode { self.outputResources = inputResources } - func execute(context: Context) -> [RenderSlotValue] { + func execute(context: inout Context, renderContext: RenderContext) -> [RenderSlotValue] { return context.inputResources } } @@ -59,7 +76,10 @@ public struct RunGraphNode: RenderNode { self.graphName = graphName } - public func execute(context: Context) async throws -> [RenderSlotValue] { + public func execute( + context: inout Context, + renderContext: RenderContext + ) async throws -> [RenderSlotValue] { context.runSubgraph(by: graphName, inputs: context.inputResources, viewEntity: context.viewEntity) return [] } @@ -73,8 +93,7 @@ public struct RunGraphNode: RenderNode { /// /// The ``RenderGraphExecutor`` is responsible for executing the entire graph each frame. /// -@RenderGraphActor -public final class RenderGraph: Resource { +public struct RenderGraph: Resource { static let entryNodeName: String = "_GraphEntryNode" @@ -101,8 +120,7 @@ public final class RenderGraph: Resource { } } - struct Node { - + struct Node: Sendable { typealias ID = String let name: String @@ -112,18 +130,31 @@ public final class RenderGraph: Resource { var outputEdges: [Edge] = [] } - public nonisolated init(label: String? = nil) { - self.label = label - } - - private(set) var label: String? + let label: String? + private let logger: Logger internal private(set) var nodes: [Node.ID: Node] = [:] internal private(set) var subGraphs: [String: RenderGraph] = [:] internal private(set) var entryNode: Node? - - public func addEntryNode(inputs: [RenderSlot]) -> String { + + public nonisolated init(label: String? = nil) { + self.label = label + self.logger = Logger(label: label.flatMap { "RenderGraph(\($0))" } ?? "RenderGraph") + } + + + public func update(from world: World) { + for node in nodes { + node.value.node.update(from: world) + } + + for graph in self.subGraphs { + graph.value.update(from: world) + } + } + + public mutating func addEntryNode(inputs: [RenderSlot]) -> String { let node = GraphEntryNode(inputResources: inputs) let renderNode = Node(name: Self.entryNodeName, node: node) self.nodes[Self.entryNodeName] = renderNode @@ -133,16 +164,16 @@ public final class RenderGraph: Resource { } @inline(__always) - public func addNode(_ node: T) { + public mutating func addNode(_ node: T) { self.addNode(node, by: T.name) } - public func addNode(_ node: RenderNode, by name: String) { + public mutating func addNode(_ node: RenderNode, by name: String) { self.nodes[name] = Node(name: name, node: node) } @inline(__always) - public func addSlotEdge( + public mutating func addSlotEdge( from: From.Type, outputSlot: String, to: To.Type, @@ -156,7 +187,7 @@ public final class RenderGraph: Resource { ) } - public func addSlotEdge( + public mutating func addSlotEdge( fromNode outputNodeName: String, outputSlot: String, toNode inputNodeName: String, @@ -172,7 +203,6 @@ public final class RenderGraph: Resource { let outputSlotIndex = oNode.node.outputResources.firstIndex(where: { $0.name == outputSlot }) let inputSlotIndex = iNode.node.inputResources.firstIndex(where: { $0.name == inputSlot }) - assert(outputSlotIndex != nil, "Can't find slot by name \(outputSlot)") assert(inputSlotIndex != nil, "Can't find slot by name \(inputSlot)") @@ -180,8 +210,13 @@ public final class RenderGraph: Resource { return } - let edge = Edge.slot(outputNode: outputNodeName, outputSlotIndex: outputSlotIndex, inputNode: inputNodeName, inputSlotIndex: inputSlotIndex) - + let edge = Edge.slot( + outputNode: outputNodeName, + outputSlotIndex: outputSlotIndex, + inputNode: inputNodeName, + inputSlotIndex: inputSlotIndex + ) + guard self.validateEdge(edge, shouldExsits: false) else { return } @@ -194,11 +229,11 @@ public final class RenderGraph: Resource { } @inline(__always) - public func addNodeEdge(from: From.Type, to: To.Type) { + public mutating func addNodeEdge(from: From.Type, to: To.Type) { self.addNodeEdge(from: From.name, to: To.name) } - public func addNodeEdge(from outputNodeName: String, to inputNodeName: String) { + public mutating func addNodeEdge(from outputNodeName: String, to inputNodeName: String) { let oNode = self.nodes[outputNodeName] let iNode = self.nodes[inputNodeName] assert(oNode != nil, "Can't find node by name \(outputNodeName)") @@ -217,11 +252,11 @@ public final class RenderGraph: Resource { } @inline(__always) - public func removeNode(by type: T.Type) -> Bool { + public mutating func removeNode(by type: T.Type) -> Bool { self.removeNode(by: T.name) } - public func removeNode(by name: String) -> Bool { + public mutating func removeNode(by name: String) -> Bool { guard let node = self.nodes.removeValue(forKey: name) else { // Node not exists return false @@ -239,7 +274,7 @@ public final class RenderGraph: Resource { } @inline(__always) - public func removeSlotEdge( + public mutating func removeSlotEdge( from: From.Type, outputSlot: String, to: To.Type, @@ -248,7 +283,7 @@ public final class RenderGraph: Resource { self.removeSlotEdge(fromNode: From.name, outputSlot: outputSlot, toNode: To.name, inputSlot: inputSlot) } - public func removeSlotEdge( + public mutating func removeSlotEdge( fromNode outputNodeName: String, outputSlot: String, toNode inputNodeName: String, @@ -263,8 +298,13 @@ public final class RenderGraph: Resource { return false } - let edge = Edge.slot(outputNode: outputNodeName, outputSlotIndex: outputSlotIndex, inputNode: inputNodeName, inputSlotIndex: inputSlotIndex) - + let edge = Edge.slot( + outputNode: outputNodeName, + outputSlotIndex: outputSlotIndex, + inputNode: inputNodeName, + inputSlotIndex: inputSlotIndex + ) + if !self.hasEdge(edge) { return false } @@ -278,7 +318,7 @@ public final class RenderGraph: Resource { return true } - public func addSubgraph(_ graph: RenderGraph, name: String) { + public mutating func addSubgraph(_ graph: RenderGraph, name: String) { self.subGraphs[name] = graph } @@ -345,7 +385,7 @@ public final class RenderGraph: Resource { } guard let oNode = self.nodes[outputNode], let iNode = self.nodes[inputNode] else { - // Nodes not exists + self.logger.error("[Validation Error] Nodes not exists. Output: \(outputNode), Input: \(inputNode)") return false } @@ -361,12 +401,12 @@ public final class RenderGraph: Resource { }) if isSlotConnected && !shouldExsits { - // Slot already connected + self.logger.error("[Validation Error] Slot already connected. Output slot: \(outputSlot.name), Input slot: \(inputSlot.name)") return false } if outputSlot.kind != inputSlot.kind { - // Mismatched types + self.logger.error("[Validation Error] Mismatched types. Output slot: \((outputSlot.name, outputSlot.kind.rawValue)), Input slot: \((inputSlot.name, inputSlot.kind.rawValue))") return false } @@ -375,7 +415,7 @@ public final class RenderGraph: Resource { } -extension RenderGraph: @preconcurrency CustomDebugStringConvertible { +extension RenderGraph: CustomDebugStringConvertible { public var debugDescription: String { var string = "\(label ?? "RenderGraph"):\n" for node in self.nodes.values { diff --git a/Sources/AdaRender/RenderGraph/RenderGraphContext.swift b/Sources/AdaRender/RenderGraph/RenderGraphContext.swift index f890d3bbc..c3f24cab0 100644 --- a/Sources/AdaRender/RenderGraph/RenderGraphContext.swift +++ b/Sources/AdaRender/RenderGraph/RenderGraphContext.swift @@ -8,33 +8,42 @@ import AdaECS import Logging +struct PendingSubGraph: Sendable { + let graph: RenderGraph + let inputs: [RenderSlotValue] + let viewEntity: Entity? +} + /// The context with all graph information required to run a ``RenderNode``. /// This context is created for each node by the ``RenderGraphExecutor``. -@RenderGraphActor -public final class RenderGraphContext { +public struct RenderGraphContext: ~Copyable, Sendable { public let graph: RenderGraph - public let device: RenderDevice public let world: World public internal(set) var inputResources: [RenderSlotValue] public let tracer: Logger public let viewEntity: Entity? - init(graph: RenderGraph, world: World, device: RenderDevice, inputResources: [RenderSlotValue], tracer: Logger, viewEntity: Entity?) { + init( + graph: RenderGraph, + world: World, + inputResources: [RenderSlotValue], + tracer: Logger, + viewEntity: Entity? + ) { self.graph = graph - self.device = device self.world = world self.tracer = tracer self.inputResources = inputResources self.viewEntity = viewEntity } - internal var pendingSubgraphs: [(renderGraph: RenderGraph, inputs: [RenderSlotValue], viewEntity: Entity?)] = [] + internal var pendingSubgraphs: [PendingSubGraph] = [] } public extension RenderGraphContext { // FIXME: Should throws error! - func runSubgraph(by name: String, inputs: [RenderSlotValue], viewEntity: Entity? = nil) { + mutating func runSubgraph(by name: String, inputs: [RenderSlotValue], viewEntity: Entity? = nil) { guard let graph = self.graph.subGraphs[name] else { return } @@ -47,7 +56,7 @@ public extension RenderGraphContext { } } - self.pendingSubgraphs.append((graph, inputs, viewEntity)) + self.pendingSubgraphs.append(PendingSubGraph(graph: graph, inputs: inputs, viewEntity: viewEntity)) } func entityResource(by name: String) -> Entity? { @@ -70,5 +79,5 @@ public extension RenderGraphContext { /// A global actor that is used to run render graph nodes. @globalActor public actor RenderGraphActor: GlobalActor { - public static var shared = RenderGraphActor() + public static let shared = RenderGraphActor() } diff --git a/Sources/AdaRender/RenderGraph/RenderGraphExecutor.swift b/Sources/AdaRender/RenderGraph/RenderGraphExecutor.swift index 2cf6a3a93..420fcb484 100644 --- a/Sources/AdaRender/RenderGraph/RenderGraphExecutor.swift +++ b/Sources/AdaRender/RenderGraph/RenderGraphExecutor.swift @@ -12,18 +12,34 @@ import Logging import Collections /// Execute ``RenderGraph`` objects. -@RenderGraphActor -public class RenderGraphExecutor { +public struct RenderGraphExecutor: Sendable { - public nonisolated init() {} + public init() {} /// Execute ``RenderGraph`` for specific ``World``. - public func execute(_ graph: RenderGraph, in world: World) async throws { - try await self.executeGraph(graph, world: world, inputResources: [], viewEntity: nil) + public func execute( + _ graph: RenderGraph, + renderDevice: RenderDevice, + in world: World + ) async throws { + let renderContext = RenderContext(device: renderDevice, commandQueue: renderDevice.createCommandQueue()) + try await self.executeGraph( + graph, + renderContext: renderContext, + world: world, + inputResources: [], + viewEntity: nil + ) } // swiftlint:disable:next cyclomatic_complexity function_body_length - private func executeGraph(_ graph: RenderGraph, world: World, inputResources: [RenderSlotValue], viewEntity: Entity?) async throws { + private func executeGraph( + _ graph: RenderGraph, + renderContext: RenderContext, + world: World, + inputResources: [RenderSlotValue], + viewEntity: Entity? + ) async throws { let tracer = Logger(label: "RenderGraph") tracer.trace("Begin Render Graph Frame") @@ -78,17 +94,22 @@ public class RenderGraphExecutor { } } let inputs = inputSlots.sorted(by: { $0.0 > $1.0 }).map { $0.1 } - let context = RenderGraphContext( + var context = RenderGraphContext( graph: graph, world: world, - device: RenderEngine.shared.renderDevice, inputResources: inputs, tracer: tracer, viewEntity: viewEntity ) - let outputs = try await currentNode.node.execute(context: context) - for (subGraph, inputValues, viewEntity) in context.pendingSubgraphs { - try await self.executeGraph(subGraph, world: world, inputResources: inputValues, viewEntity: viewEntity) + let outputs = try await currentNode.node.execute(context: &context, renderContext: renderContext) + for subGraph in context.pendingSubgraphs { + try await self.executeGraph( + subGraph.graph, + renderContext: renderContext, + world: world, + inputResources: subGraph.inputs, + viewEntity: subGraph.viewEntity + ) } precondition(outputs.count == currentNode.node.outputResources.count) diff --git a/Sources/AdaRender/RenderGraph/RenderNode.swift b/Sources/AdaRender/RenderGraph/RenderNode.swift index f0ece656e..c9edc8f37 100644 --- a/Sources/AdaRender/RenderGraph/RenderNode.swift +++ b/Sources/AdaRender/RenderGraph/RenderNode.swift @@ -5,12 +5,13 @@ // Created by v.prusakov on 2/18/23. // +import AdaECS + /// A render node that can be added to a ``RenderGraph``. /// /// Nodes are the fundamental part of the graph and used to extend its functionality, by /// generating draw calls and/or running subgraphs. -@RenderGraphActor -public protocol RenderNode { +public protocol RenderNode: Sendable { typealias Context = RenderGraphContext @@ -23,9 +24,16 @@ public protocol RenderNode { /// Runtime key for link slot and node together. static var name: String { get } + /// Update graph states from given world. + func update(from world: World) + /// Execute the graph node logic, issues draw calls, updates the output slots and optionally queues up subgraphs for execution. The graph data, input and output values are /// passed via the ``RenderGraphContext``. - func execute(context: Context) async throws -> [RenderSlotValue] + @RenderGraphActor + func execute( + context: inout Context, + renderContext: RenderContext + ) async throws -> [RenderSlotValue] } public extension RenderNode { @@ -35,17 +43,15 @@ public extension RenderNode { } public extension RenderNode { - var inputResources: [RenderSlot] { - return [] - } - - var outputResources: [RenderSlot] { - return [] - } + var inputResources: [RenderSlot] { return [] } + + var outputResources: [RenderSlot] { return [] } + + func update(from world: World) { } } public struct EmptyRenderNode: RenderNode { - public func execute(context: Context) async throws -> [RenderSlotValue] { + public func execute(context: inout Context, renderContext: RenderContext) async throws -> [RenderSlotValue] { return [] } } diff --git a/Sources/AdaRender/RenderGraph/RenderResource.swift b/Sources/AdaRender/RenderGraph/RenderResource.swift index f577d8432..01aae0f82 100644 --- a/Sources/AdaRender/RenderGraph/RenderResource.swift +++ b/Sources/AdaRender/RenderGraph/RenderResource.swift @@ -7,14 +7,14 @@ import AdaECS -public enum RenderResource { +public enum RenderResource: Sendable { case texture(Texture) case buffer(Buffer) case sampler(Sampler) case entity(Entity) } -public enum RenderResourceKind { +public enum RenderResourceKind: String, Sendable { case texture case buffer case sampler diff --git a/Sources/AdaRender/RenderItems/DrawPass.swift b/Sources/AdaRender/RenderItems/DrawPass.swift index af2fbc9d2..e1100718b 100644 --- a/Sources/AdaRender/RenderItems/DrawPass.swift +++ b/Sources/AdaRender/RenderItems/DrawPass.swift @@ -7,24 +7,19 @@ import AdaECS -/// The context with information required to run a ``DrawPass``. -public struct RenderContext { - public let device: RenderDevice - public let entity: Entity - public let world: World - public let view: Entity - public let drawList: DrawList -} - /// Draw pass is a render function that will render for specific item. /// /// For example, you can create render pass for rendering ``Transparent2DRenderItem`` and configure rendering whatever you want. /// Pass additional render data as components to ``Entity`` and pass that entity to ``Transparent2DRenderItem/entity`` property. public protocol DrawPass: Resource { associatedtype Item: RenderItem - typealias Context = RenderContext - func render(in context: Context, item: Item) throws + func render( + with renderEncoder: RenderCommandEncoder, + world: World, + view: Entity, + item: Item + ) throws } /// Type-erased draw pass. @@ -35,13 +30,33 @@ public struct AnyDrawPass: DrawPass { self.base = base } - public func render(in context: Context, item: T) throws { - try base._render(in: context, item: item) + public func render( + with renderEncoder: RenderCommandEncoder, + world: World, + view: Entity, + item: T + ) throws { + try base._render( + with: renderEncoder, + world: world, + view: view, + item: item + ) } } private extension DrawPass { - func _render(in context: Context, item: Any) throws { - try self.render(in: context, item: item as! Self.Item) + func _render( + with renderEncoder: RenderCommandEncoder, + world: World, + view: Entity, + item: Any + ) throws { + try self.render( + with: renderEncoder, + world: world, + view: view, + item: item as! Self.Item + ) } } diff --git a/Sources/AdaRender/RenderItems/RenderItem.swift b/Sources/AdaRender/RenderItems/RenderItem.swift index ea339a983..138c3b7da 100644 --- a/Sources/AdaRender/RenderItems/RenderItem.swift +++ b/Sources/AdaRender/RenderItems/RenderItem.swift @@ -40,18 +40,14 @@ public struct RenderItems: Sendable { /// - drawList: The draw list. /// - world: The world. /// - view: The view. - public func render(_ drawList: DrawList, world: World, view: Entity) throws { + public func render(with renderPass: RenderCommandEncoder, world: World, view: Entity) throws { for item in self.items { - let context = RenderContext( - device: drawList.renderDevice, - entity: item.entity, + try AnyDrawPass(item.drawPass).render( + with: renderPass, world: world, view: view, - drawList: drawList + item: item ) - - try AnyDrawPass(item.drawPass).render(in: context, item: item) - drawList.clear() } } } @@ -62,7 +58,7 @@ public protocol RenderItem: Sendable { associatedtype SortKey: Comparable /// The entity of the render item. - var entity: Entity { get } + var entity: Entity.ID { get } /// The draw pass of the render item. var drawPass: any DrawPass { get } diff --git a/Sources/AdaRender/RenderPipelines.swift b/Sources/AdaRender/RenderPipelines.swift new file mode 100644 index 000000000..b02be4bf2 --- /dev/null +++ b/Sources/AdaRender/RenderPipelines.swift @@ -0,0 +1,57 @@ +// +// RenderPipelines.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 05.12.2025. +// + +import AdaECS + +public protocol RenderPipelineConfigurator: Resource { + associatedtype Configuration: Hashable, Sendable + + func configurate(with configuration: Configuration) -> RenderPipelineDescriptor +} + +public struct RenderPipelineEmptyConfiguration: Hashable, Sendable {} + +public struct RenderPipelines: Resource { + private var caches: [T.Configuration: RenderPipeline] = [:] + let configurator: T + + public init(configurator: T) { + self.configurator = configurator + } + + public mutating func pipeline( + for configuration: T.Configuration, + device: RenderDevice + ) -> any RenderPipeline { + if let pipeline = self.caches[configuration] { + return pipeline + } + + let pipelineDesc = configurator.configurate(with: configuration) + let pipeline = device.createRenderPipeline(from: pipelineDesc) + caches[configuration] = pipeline + return pipeline + } + + public mutating func dropCache() { + self.caches.removeAll(keepingCapacity: true) + } +} + +extension RenderPipelines where T.Configuration == RenderPipelineEmptyConfiguration { + public mutating func pipeline( + device: RenderDevice + ) -> any RenderPipeline { + self.pipeline(for: RenderPipelineEmptyConfiguration(), device: device) + } +} + +extension RenderPipelines: WorldInitable where T: WorldInitable { + public init(from world: World) { + self.configurator = T.init(from: world) + } +} diff --git a/Sources/AdaRender/RenderWorldPlugin.swift b/Sources/AdaRender/RenderWorldPlugin.swift index b10a198f7..12e614ad2 100644 --- a/Sources/AdaRender/RenderWorldPlugin.swift +++ b/Sources/AdaRender/RenderWorldPlugin.swift @@ -8,10 +8,11 @@ import AdaApp import AdaECS import AdaUtils +import Foundation +import Math /// The plugin that sets up the render world. public struct RenderWorldPlugin: Plugin { - public init() {} /// Setup the render world. @@ -23,52 +24,96 @@ public struct RenderWorldPlugin: Plugin { BoundingComponent.registerComponent() Texture.registerTypes() - let renderWorld = app.createSubworld(by: .renderWorld) - renderWorld.insertResource(RenderGraph(label: "RenderWorld_Root")) + let renderWorld = AppWorlds(main: World(name: "RenderWorld")) + renderWorld.updateScheduler = .renderRunner + renderWorld + .insertResource(RenderGraph(label: "RenderWorld_Root")) + .insertResource(DefaultSchedulerOrder(order: [ + .preUpdate, + .prepare, + .update, + .render, + .postUpdate + ])) renderWorld.setExctractor(RenderWorldExctractor()) - renderWorld.mainWorld.setSchedulers([ + renderWorld.main.setSchedulers([ + .extract, + .preUpdate, + .prepare, .update, - .render + .render, + .postUpdate ]) - renderWorld.insertResource( - DefaultSchedulerOrder( - order: [.update, .render] - ) - ) - renderWorld.mainWorld - .addSystem(RenderWorldSystem.self, on: .render) + renderWorld + .insertResource(RenderDeviceHandler(renderDevice: RenderEngine.shared.renderDevice)) + .insertResource(WindowSurfaces(windows: [:])) + .addSystem(CreateWindowSurfacesSystem.self, on: .prepare) + .addSystem(DefaultSchedulerRunner.self, on: .renderRunner) + .addSystem(RenderSystem.self, on: .render) + + app.addSubworld(renderWorld, by: .renderWorld) + app.addPlugin(UpscalePlugin()) } } -/// The system that renders the world. -@System -struct RenderWorldSystem { +public struct RenderDeviceHandler: Resource { + public var renderDevice: RenderDevice + + public init(renderDevice: RenderDevice) { + self.renderDevice = renderDevice + } +} - private let renderGraphExecutor = RenderGraphExecutor() +public struct RenderEngineHandler: Resource { + public var renderEngine: RenderEngine - init(world: World) { } + public init(renderEngine: RenderEngine) { + self.renderEngine = renderEngine + } +} - @ResQuery - private var renderGraph: RenderGraph! +@System +func Render( + _ context: WorldUpdateContext, + _ renderGraph: Res, + _ surfaces: Res, + _ renderDevice: Res +) { + let world = context.world + let renderGraph = renderGraph.wrappedValue + renderGraph?.update(from: world) + + // We should capture drawables before we start async task. + // Because we can have a situation when we start task, but main thread already cleared surfaces. + let windows = surfaces.windows + + Task { @RenderGraphActor [renderGraph] in + do { + guard + let renderGraph, + let renderDevice = renderDevice.wrappedValue?.renderDevice + else { + return + } + let renderGraphExecutor = RenderGraphExecutor() + try await renderGraphExecutor.execute(renderGraph, renderDevice: renderDevice, in: world) - func update(context: inout UpdateContext) { - let world = context.world - context.taskGroup.addTask { - do { - try await self.renderGraphExecutor.execute(renderGraph, in: world) - } catch { - print(error) + for window in windows { + try window.currentDrawable?.present() } + } catch { + assertionFailure("Failed to execute render graph \(error)") } } } /// The extractor that extracts the main world to the render world. struct RenderWorldExctractor: WorldExctractor { - func exctract(from mainWorld: World, to renderWorld: World) { + func exctract(from mainWorld: World, to renderWorld: World) async { renderWorld.clear() renderWorld.insertResource(MainWorld(world: mainWorld)) + await renderWorld.runScheduler(.extract) } } @@ -77,44 +122,92 @@ struct MainWorld: Resource { var world: World } -/// A property wrapper that allows you to extract a resource from the main world. -@propertyWrapper -public final class Extract: @unchecked Sendable { - private var _value: T! - public var wrappedValue: T { - self._value +public struct WindowSurface: Sendable { + public var swapchain: (any Swapchain)? + public var currentDrawable: (any Drawable)? +} + +public struct WindowSurfaces: Resource { + public var windows: SparseSet +} + +@System +func CreateWindowSurfaces( + _ surfaces: ResMut, + _ renderDevice: Res, + _ primaryWindow: Extract> +) async { + surfaces.windows.removeAll() + let device = renderDevice.renderDevice + + do { + let renderWindows = try await RenderEngine.shared.getRenderWindows() + for (windowId, _) in renderWindows.windows.values { + let swapchain = await device.createSwapchain(from: windowId) + + let ref: WindowRef = if primaryWindow.wrappedValue.windowId == windowId { + .primary + } else { + .windowId(windowId) + } + surfaces.windows[ref] = WindowSurface( + swapchain: swapchain, + currentDrawable: swapchain.getNextDrawable(device) + ) + } + } catch { + print("CreateWindowSurfaces", error.localizedDescription) } +} - /// Initialize a new extract. - public init() { } +public struct PrimaryWindowId: Resource { + public var windowId: RID - /// Initialize a new extract. - /// - Parameter from: The world to extract the resource from. - public init(from world: World) { - self._value = T.init(from: world) + public init(windowId: RID) { + self.windowId = windowId } +} + +public struct RenderWindows: Resource { + public var windows: SparseSet - /// Call the extract. - /// - Returns: The extracted resource. - public func callAsFunction() -> T { - self._value + public init(windows: SparseSet) { + self.windows = windows } } -extension Extract: SystemQuery { - public func update(from world: World) { - if _value == nil { - _value = T.init(from: world) - } - if let resource = world.getResource(MainWorld.self) { - _value?.update(from: resource.world) - } +public struct RenderWindow: Sendable, Hashable { + public var windowId: WindowID + public var height: Int + public var width: Int + public var scaleFactor: Float + + public var physicalSize: Size { + Size( + width: Float(width) * scaleFactor, + height: Float(height) * scaleFactor + ) + } + + public var logicalSize: SizeInt { + SizeInt(width: width, height: height) + } + + public init(windowId: WindowID, height: Int, width: Int, scaleFactor: Float) { + self.windowId = windowId + self.height = height + self.width = width + self.scaleFactor = scaleFactor } } public extension SchedulerName { /// The render scheduler. + static let renderRunner = SchedulerName(rawValue: "RenderWorld_RenderRunner") + + static let prepare = SchedulerName(rawValue: "RenderWorld_Prepare") static let render = SchedulerName(rawValue: "RenderWorld_Render") + static let extract = SchedulerName(rawValue: "RenderWorld_Extract") } public extension AppWorldName { diff --git a/Sources/AdaRender/Scene2DPlugin.swift b/Sources/AdaRender/Scene2DPlugin.swift deleted file mode 100644 index 4030e5dfa..000000000 --- a/Sources/AdaRender/Scene2DPlugin.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// Scene2DPlugin.swift -// AdaEngine -// -// Created by v.prusakov on 2/19/23. -// - -import AdaApp -import AdaECS - -/// Plugin for RenderWorld added 2D render capatibilites. -public struct Scene2DPlugin: Plugin { - - /// Render graph name. - public static let renderGraph = "render_graph_2d" - - public init() {} - - /// Input slots of render graph. - public enum InputNode { - public static let view = "view" - } - - public func setup(in app: AppWorlds) { - guard let app = app.getSubworldBuilder(by: .renderWorld) else { - return - } - - // Add Systems - app.addSystem(BatchTransparent2DItemsSystem.self) - - Task { @RenderGraphActor in - // Add Render graph - let graph = RenderGraph(label: "Scene2D") - - let entryNode = graph.addEntryNode(inputs: [ - RenderSlot(name: InputNode.view, kind: .entity) - ]) - - graph.addNode(Main2DRenderNode()) - graph.addSlotEdge( - fromNode: entryNode, - outputSlot: InputNode.view, - toNode: Main2DRenderNode.name, - inputSlot: Main2DRenderNode.InputNode.view - ) - - await app.mainWorld - .getResource(RenderGraph.self)? - .addSubgraph(graph, name: Self.renderGraph) - } - } -} - -/// This render node responsible for rendering ``Transparent2DRenderItem``. -public struct Main2DRenderNode: RenderNode { - - /// Input slots of render node. - public enum InputNode { - public static let view = "view" - } - - public init() {} - - public let inputResources: [RenderSlot] = [ - RenderSlot(name: InputNode.view, kind: .entity) - ] - - public func execute(context: Context) async throws -> [RenderSlotValue] { - guard let entity = context.entityResource(by: InputNode.view) else { - return [] - } - - let (camera, renderItems) = entity.components[Camera.self, RenderItems.self] - if - case .window(let windowRef) = camera.renderTarget, - case .windowId(let id) = windowRef, - id == .empty - { - return [] - } - - let sortedRenderItems = renderItems.sorted() - let clearColor = camera.clearFlags.contains(.solid) ? camera.backgroundColor : .surfaceClearColor - - let drawList: DrawList - switch camera.renderTarget { - case .window(let windowId): - drawList = try context.device.beginDraw( - for: windowId, - clearColor: clearColor, - loadAction: .clear, - storeAction: .store - ) - case .texture(let textureHandle): - let texture = textureHandle.asset - let desc = FramebufferDescriptor( - scale: texture.scaleFactor, - width: texture.width, - height: texture.height, - attachments: [ - FramebufferAttachmentDescriptor( - format: texture.pixelFormat, - texture: texture, - clearColor: clearColor, - loadAction: .clear, - storeAction: .store - ) - ] - ) - let framebuffer = context.device.createFramebuffer(from: desc) - drawList = try context.device.beginDraw(to: framebuffer, clearColors: []) - } - - if let viewport = camera.viewport { - drawList.setViewport(viewport) - } - - try sortedRenderItems.render(drawList, world: context.world, view: entity) - context.device.endDrawList(drawList) - return [] - } -} - -/// An object describe 2D render item. -public struct Transparent2DRenderItem: RenderItem { - - /// An entity that hold additional information about render item. - public var entity: Entity - - /// An entity for batch rendering. - public var batchEntity: Entity - - /// Draw pass which will be used for rendering this item. - public var drawPass: any DrawPass - - /// Render Pipeline for rendering this item. - public var renderPipeline: RenderPipeline - - /// Sort key used for rendering order. - public var sortKey: Float - - /// If item support batch rendering, pass range of indecies. - public var batchRange: Range? - - public init( - entity: Entity, - batchEntity: Entity, - drawPass: any DrawPass, - renderPipeline: RenderPipeline, - sortKey: Float, - batchRange: Range? = nil - ) { - self.entity = entity - self.batchEntity = batchEntity - self.drawPass = drawPass - self.renderPipeline = renderPipeline - self.sortKey = sortKey - self.batchRange = batchRange - } -} diff --git a/Sources/AdaRender/Shaders/Shader.swift b/Sources/AdaRender/Shaders/Shader.swift index 5eb913fab..f9fb7b005 100644 --- a/Sources/AdaRender/Shaders/Shader.swift +++ b/Sources/AdaRender/Shaders/Shader.swift @@ -7,8 +7,11 @@ import AdaAssets import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation - +#endif /// Contains native compiled GPU device shader. public protocol CompiledShader: AnyObject {} diff --git a/Sources/AdaRender/Shaders/ShaderCache.swift b/Sources/AdaRender/Shaders/ShaderCache.swift index 9157913ff..926d9d615 100644 --- a/Sources/AdaRender/Shaders/ShaderCache.swift +++ b/Sources/AdaRender/Shaders/ShaderCache.swift @@ -6,8 +6,12 @@ // import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation -@preconcurrency import Yams +#endif +@unsafe @preconcurrency import Yams /// Contains information about shader changes and store/load spirv binary in cache folder. enum ShaderCache { diff --git a/Sources/AdaRender/Shaders/ShaderCompiler.swift b/Sources/AdaRender/Shaders/ShaderCompiler.swift index 506519cd4..431321c20 100644 --- a/Sources/AdaRender/Shaders/ShaderCompiler.swift +++ b/Sources/AdaRender/Shaders/ShaderCompiler.swift @@ -5,7 +5,12 @@ // Created by v.prusakov on 3/10/23. // +import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import SPIRVCompiler import SPIRV_Cross @@ -165,11 +170,11 @@ public final class ShaderCompiler { var error: UnsafePointer? let defines = self.getDefines(for: stage) - let binary = defines.withCString { definesPtr in - let options = spirv_options(preamble: definesPtr) + let binary = unsafe defines.withCString { definesPtr in + let options = unsafe spirv_options(preamble: definesPtr) - return code.withCString { sourcePtr in - compile_shader_glsl( + return unsafe code.withCString { sourcePtr in + unsafe compile_shader_glsl( sourcePtr, /* source */ stage.toShaderCompiler, /* stage */ options, /* options */ @@ -178,14 +183,14 @@ public final class ShaderCompiler { } } - if let error { - let message = String(cString: error, encoding: .utf8) ?? "Failed to compile" + if let error = unsafe error { + let message = unsafe String(cString: error, encoding: .utf8) ?? "Failed to compile" throw CompileError.glslError(message) } - let data = Data(bytes: binary.bytes, count: Int(binary.length)) - binary.bytes.deallocate() - + let data = unsafe Data(bytes: binary.bytes, count: Int(binary.length)) + unsafe binary.bytes.deallocate() + return SpirvBinary( stage: stage, data: data, diff --git a/Sources/AdaRender/Shaders/ShaderIncluder.swift b/Sources/AdaRender/Shaders/ShaderIncluder.swift index b6ad7a45e..0e15c96f7 100644 --- a/Sources/AdaRender/Shaders/ShaderIncluder.swift +++ b/Sources/AdaRender/Shaders/ShaderIncluder.swift @@ -6,7 +6,11 @@ // import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif // TODO: Make include depth (with cycle detection) // TODO: Optimizations, decrease loops diff --git a/Sources/AdaRender/Shaders/ShaderModule.swift b/Sources/AdaRender/Shaders/ShaderModule.swift index 4be5f2920..dfe49eb29 100644 --- a/Sources/AdaRender/Shaders/ShaderModule.swift +++ b/Sources/AdaRender/Shaders/ShaderModule.swift @@ -7,7 +7,11 @@ import AdaAssets import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// A shader module that stores shaders. public final class ShaderModule: Asset, @unchecked Sendable { diff --git a/Sources/AdaRender/Shaders/ShaderResource.swift b/Sources/AdaRender/Shaders/ShaderResource.swift index 7a6e51afb..7009826fb 100644 --- a/Sources/AdaRender/Shaders/ShaderResource.swift +++ b/Sources/AdaRender/Shaders/ShaderResource.swift @@ -124,8 +124,8 @@ extension ShaderResource.ResourceType { extension ShaderValueType { // swiftlint:disable:next cyclomatic_complexity init?(typeId: spvc_type_id, compiler: spvc_compiler) { - let type = spvc_compiler_get_type_handle(compiler, typeId) - let baseType = spvc_type_get_basetype(type) + let type = unsafe spvc_compiler_get_type_handle(compiler, typeId) + let baseType = unsafe spvc_type_get_basetype(type) switch baseType { case SPVC_BASETYPE_BOOLEAN: self = .bool @@ -134,8 +134,8 @@ extension ShaderValueType { case SPVC_BASETYPE_UINT8: self = .char case SPVC_BASETYPE_FP32: - let vectorCount = spvc_type_get_vector_size(type) - let columnCount = spvc_type_get_columns(type) + let vectorCount = unsafe spvc_type_get_vector_size(type) + let columnCount = unsafe spvc_type_get_columns(type) if columnCount == 3 { self = .mat3 diff --git a/Sources/AdaRender/Shaders/ShaderSource.swift b/Sources/AdaRender/Shaders/ShaderSource.swift index e881c37f9..a71aad221 100644 --- a/Sources/AdaRender/Shaders/ShaderSource.swift +++ b/Sources/AdaRender/Shaders/ShaderSource.swift @@ -7,7 +7,11 @@ import AdaAssets import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import SPIRVCompiler public enum ShaderLanguage: String { diff --git a/Sources/AdaRender/Shaders/ShaderUtils.swift b/Sources/AdaRender/Shaders/ShaderUtils.swift index e789e2c35..d65a30e59 100644 --- a/Sources/AdaRender/Shaders/ShaderUtils.swift +++ b/Sources/AdaRender/Shaders/ShaderUtils.swift @@ -9,7 +9,11 @@ // Currently Swift Regex doesn't supports on macOS less than 13.0 and that's a problem here. // I want to use swift like solution instead of Foundation. +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// Collection of utils for works with shaders. enum ShaderUtils { diff --git a/Sources/AdaRender/Shaders/SpirvCompiler.swift b/Sources/AdaRender/Shaders/SpirvCompiler.swift index 47fd477d9..0f0bca56f 100644 --- a/Sources/AdaRender/Shaders/SpirvCompiler.swift +++ b/Sources/AdaRender/Shaders/SpirvCompiler.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 3/13/23. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import SPIRV_Cross import Logging @@ -22,6 +26,7 @@ struct SpirvShader { } /// Create High Level Shading Language from SPIR-V for specific shader language. +@safe final class SpirvCompiler { let deviceLang: ShaderLanguage @@ -53,25 +58,25 @@ final class SpirvCompiler { self.stage = stage var context: spvc_context! - spvc_context_create(&context) + unsafe spvc_context_create(&context) var ir: spvc_parsed_ir! - let result = spriv.withUnsafeBytes { spvPtr in - let spv = spvPtr.bindMemory(to: SpvId.self) - return spvc_context_parse_spirv(context, spv.baseAddress, spv.count, &ir) + let result = unsafe spriv.withUnsafeBytes { spvPtr in + let spv = unsafe spvPtr.bindMemory(to: SpvId.self) + return unsafe spvc_context_parse_spirv(context, spv.baseAddress, spv.count, &ir) } if result != SPVC_SUCCESS { - throw Error(String(cString: spvc_context_get_last_error_string(context))) + throw unsafe Error(String(cString: spvc_context_get_last_error_string(context))) } - self.ir = ir - self.context = context - + unsafe self.ir = ir + unsafe self.context = context + self.deviceLang = deviceLang var spvcCompiler: spvc_compiler? - spvc_context_create_compiler( + unsafe spvc_context_create_compiler( context, deviceLang.spvcBackend, ir, @@ -79,34 +84,34 @@ final class SpirvCompiler { &spvcCompiler ) - guard let spvcCompiler else { - throw Error(String(cString: spvc_context_get_last_error_string(context))) + guard let spvcCompiler = unsafe spvcCompiler else { + throw unsafe Error(String(cString: spvc_context_get_last_error_string(context))) } - self.spvcCompiler = spvcCompiler + unsafe self.spvcCompiler = spvcCompiler } deinit { - spvc_context_destroy(context) + unsafe spvc_context_destroy(context) } /// Compile shader to device specific language func compile() throws -> SpirvShader { var spvcCompilerOptions: spvc_compiler_options? - if spvc_compiler_create_compiler_options(spvcCompiler, &spvcCompilerOptions) != SPVC_SUCCESS { - let errorMessage = String(cString: spvc_context_get_last_error_string(context)) + if unsafe spvc_compiler_create_compiler_options(spvcCompiler, &spvcCompilerOptions) != SPVC_SUCCESS { + let errorMessage = unsafe String(cString: spvc_context_get_last_error_string(context)) loggerShader.critical("⚠️ SPIRV-Cross compiler options creation failed: \(errorMessage)") throw Error(errorMessage) } - Self.makeCompileOptions(spvcCompilerOptions, deviceLang: deviceLang) + unsafe Self.makeCompileOptions(spvcCompilerOptions, deviceLang: deviceLang) - spvc_compiler_install_compiler_options(spvcCompiler, spvcCompilerOptions) + unsafe spvc_compiler_install_compiler_options(spvcCompiler, spvcCompilerOptions) var compilerOutputSourcePtr: UnsafePointer? - let result = spvc_compiler_compile(spvcCompiler, &compilerOutputSourcePtr) + let result = unsafe spvc_compiler_compile(spvcCompiler, &compilerOutputSourcePtr) if result != SPVC_SUCCESS { - let errorMessage = String(cString: spvc_context_get_last_error_string(context)) + let errorMessage = unsafe String(cString: spvc_context_get_last_error_string(context)) loggerShader.critical("⚠️ SPIRV-Cross compilation failed: \(errorMessage)") // Print detailed diagnostic info @@ -116,13 +121,13 @@ final class SpirvCompiler { // If we have entry points, print them var numberOfEntryPoints: Int = 0 var spvcEntryPoints: UnsafePointer? - spvc_compiler_get_entry_points(spvcCompiler, &spvcEntryPoints, &numberOfEntryPoints) - + unsafe spvc_compiler_get_entry_points(spvcCompiler, &spvcEntryPoints, &numberOfEntryPoints) + if numberOfEntryPoints > 0 { loggerShader.critical("🔍 Entry points:") for index in 0..? - spvc_compiler_get_entry_points(spvcCompiler, &spvcEntryPoints, &numberOfEntryPoints) + unsafe spvc_compiler_get_entry_points(spvcCompiler, &spvcEntryPoints, &numberOfEntryPoints) var entryPoints: [SpirvShader.EntryPoint] = [] for index in 0..? - spvc_compiler_get_entry_points(spvcCompiler, &spvcEntryPoints, &numberOfEntryPoints) + unsafe spvc_compiler_get_entry_points(spvcCompiler, &spvcEntryPoints, &numberOfEntryPoints) for index in 0.. ShaderReflectionData { var shaderResources: spvc_resources! - spvc_compiler_create_shader_resources(self.spvcCompiler, &shaderResources) + unsafe spvc_compiler_create_shader_resources(self.spvcCompiler, &shaderResources) var reflectionData = ShaderReflectionData() @@ -198,23 +203,23 @@ final class SpirvCompiler { var reflectedResources : UnsafePointer! var reflectedResourceCount = 0 - spvc_resources_get_resource_list_for_type(shaderResources, resourceType.spvcResourceType, &reflectedResources, &reflectedResourceCount) + unsafe spvc_resources_get_resource_list_for_type(shaderResources, resourceType.spvcResourceType, &reflectedResources, &reflectedResourceCount) for index in 0..= reflectionData.descriptorSets.count { reflectionData.descriptorSets.append(ShaderResource.DescriptorSet()) @@ -226,18 +231,18 @@ final class SpirvCompiler { case .uniformBuffer, .pushConstantBuffer: var members = [String: ShaderResource.ShaderBufferMember]() - let memberTypesCount = spvc_type_get_num_member_types(type) + let memberTypesCount = unsafe spvc_type_get_num_member_types(type) for index in 0 ..< memberTypesCount { - let memberType = spvc_type_get_member_type(type, index) - let memberName = String(cString: spvc_compiler_get_member_name(self.spvcCompiler, resource.base_type_id, index)) + let memberType = unsafe spvc_type_get_member_type(type, index) + let memberName = unsafe String(cString: spvc_compiler_get_member_name(self.spvcCompiler, resource.base_type_id, index)) var memberSize: Int = 0 - spvc_compiler_get_declared_struct_member_size(self.spvcCompiler, type, index, &memberSize) + unsafe spvc_compiler_get_declared_struct_member_size(self.spvcCompiler, type, index, &memberSize) var memberOffset: UInt32 = 0 - spvc_compiler_type_struct_member_offset(self.spvcCompiler, type, index, &memberOffset) + unsafe spvc_compiler_type_struct_member_offset(self.spvcCompiler, type, index, &memberOffset) - members[memberName] = ShaderResource.ShaderBufferMember( + members[memberName] = unsafe ShaderResource.ShaderBufferMember( name: memberName, size: memberSize, binding: Int(binding), @@ -258,11 +263,11 @@ final class SpirvCompiler { descriptorSet.uniformsBuffers[Int(binding)] = buffer reflectionData.shaderBuffers[resourceName] = buffer case .image, .sampler, .inputAttachment, .storageImage, .sampledImage: - let access = spvc_type_get_image_access_qualifier(type) - let isArray = spvc_type_get_image_arrayed(type) == 1 - let isMultisampled = spvc_type_get_image_multisampled(type) == 1 - let dimension = spvc_type_get_image_dimension(type) - let arraySize = spvc_type_get_array_dimension(type, 0) + let access = unsafe spvc_type_get_image_access_qualifier(type) + let isArray = unsafe spvc_type_get_image_arrayed(type) == 1 + let isMultisampled = unsafe spvc_type_get_image_multisampled(type) == 1 + let dimension = unsafe spvc_type_get_image_dimension(type) + let arraySize = unsafe spvc_type_get_array_dimension(type, 0) let resourceAccess: ShaderResource.ResourceAccess @@ -326,8 +331,8 @@ extension SpirvCompiler { } if deviceLang == .msl { - spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_MSL_VERSION, version(2, 1, 0)) - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_MSL_ENABLE_POINT_SIZE_BUILTIN, 1) + unsafe spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_MSL_VERSION, version(2, 1, 0)) + unsafe spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_MSL_ENABLE_POINT_SIZE_BUILTIN, 1) #if os(macOS) let platform = SPVC_MSL_PLATFORM_MACOS @@ -335,18 +340,18 @@ extension SpirvCompiler { let platform = SPVC_MSL_PLATFORM_IOS #endif - spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_MSL_PLATFORM, platform.rawValue) - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING, 1) + unsafe spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_MSL_PLATFORM, platform.rawValue) + unsafe spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING, 1) } if deviceLang == .glsl { // Set GLSL version to 4.10, matching our OpenGL context - spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 410) - + unsafe spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 410) + // Enable GLSL specific options for better compatibility - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_SEPARATE_SHADER_OBJECTS, 1) - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ENABLE_420PACK_EXTENSION, 1) - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, 0) // Use desktop GLSL, not GLSL ES + unsafe spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_SEPARATE_SHADER_OBJECTS, 1) + unsafe spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ENABLE_420PACK_EXTENSION, 1) + unsafe spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, 0) // Use desktop GLSL, not GLSL ES } } } diff --git a/Sources/AdaRender/Texture/AnimatedTexture.swift b/Sources/AdaRender/Texture/AnimatedTexture.swift index 032cb9373..e77a0561d 100644 --- a/Sources/AdaRender/Texture/AnimatedTexture.swift +++ b/Sources/AdaRender/Texture/AnimatedTexture.swift @@ -8,6 +8,11 @@ import AdaAssets import AdaUtils import Math +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif // TODO: Make encoding/decoding for scene serialization @@ -86,8 +91,13 @@ public final class AnimatedTexture: Texture2D, @unchecked Sendable { public init() { self.frames = [Frame].init(repeating: Frame(texture: nil, delay: 0), count: 256) let sampler = RenderEngine.shared.renderDevice.createSampler(from: SamplerDescriptor()) - super.init(gpuTexture: GPUTexture(), sampler: sampler, size: .zero) - + let texture = RenderEngine.shared.renderDevice.createTexture( + from: TextureDescriptor( + textureUsage: .read, + textureType: .texture2D + ) + ) + super.init(gpuTexture: texture, sampler: sampler, size: .zero) self.mainLoopToken = EventManager.default.subscribe( to: EngineEvents.MainLoopBegan.self, completion: update(_:) @@ -99,7 +109,7 @@ public final class AnimatedTexture: Texture2D, @unchecked Sendable { struct AssetRepresentation: Codable { struct Frame: Codable { let texture: AssetHandle // FIXME: (Vlad) resource id/path - let delay: TimeInterval + let delay: AdaUtils.TimeInterval } let frames: [Frame] @@ -108,13 +118,13 @@ public final class AnimatedTexture: Texture2D, @unchecked Sendable { let options: Options } - public convenience required init(from decoder: AssetDecoder) throws { + public convenience required init(from decoder: AssetDecoder) async throws { guard Self.extensions().contains(where: { decoder.assetMeta.filePath.pathExtension == $0 }) else { throw AssetDecodingError.invalidAssetExtension(decoder.assetMeta.filePath.pathExtension) } let asset = try decoder.decode(AssetRepresentation.self) - + self.init() self.framesCount = asset.framesCount @@ -127,7 +137,7 @@ public final class AnimatedTexture: Texture2D, @unchecked Sendable { } } - public override func encodeContents(with encoder: any AssetEncoder) throws { + public override func encodeContents(with encoder: any AssetEncoder) async throws { guard var container = encoder.encoder?.singleValueContainer() else { return } @@ -185,19 +195,19 @@ public final class AnimatedTexture: Texture2D, @unchecked Sendable { return self.frames[frame].texture } - public func setDelay(_ delay: TimeInterval, for frame: Int) { + public func setDelay(_ delay: AdaUtils.TimeInterval, for frame: Int) { self.frames[frame].delay = delay } - public func getDelay(for frame: Int) -> TimeInterval { + public func getDelay(for frame: Int) -> AdaUtils.TimeInterval { return self.frames[frame].delay } // MARK: - Private // FIXME: After breakpoint can increase animation speed. - private var time: TimeInterval = 0 - + private var time: AdaUtils.TimeInterval = 0 + /// Called each frame to update current frame. private func update(_ event: EngineEvents.MainLoopBegan) { if self.isPaused { @@ -211,7 +221,7 @@ public final class AnimatedTexture: Texture2D, @unchecked Sendable { self.time += event.deltaTime - let limit: TimeInterval = TimeInterval(self.framesPerSecond != 0 ? 1 / self.framesPerSecond : 0) + let limit: AdaUtils.TimeInterval = AdaUtils.TimeInterval(self.framesPerSecond != 0 ? 1 / self.framesPerSecond : 0) let frameTime = limit + self.frames[self.currentFrame].delay if self.time > frameTime { diff --git a/Sources/AdaRender/Texture/RenderTexture.swift b/Sources/AdaRender/Texture/RenderTexture.swift index 68ecbb46b..938bdae3f 100644 --- a/Sources/AdaRender/Texture/RenderTexture.swift +++ b/Sources/AdaRender/Texture/RenderTexture.swift @@ -58,6 +58,17 @@ public final class RenderTexture: Texture2D, @unchecked Sendable { fatalError("init(asset:) has not been implemented") } + /// Initialize a new render texture from a GPU texture. + internal init(gpuTexture: GPUTexture, format: PixelFormat, scaleFactor: Float = 1.0) { + self.pixelFormat = format + self.scaleFactor = scaleFactor + + let device = RenderEngine.shared.renderDevice + let sampler = device.createSampler(from: SamplerDescriptor()) + + super.init(gpuTexture: gpuTexture, sampler: sampler, size: gpuTexture.size) + } + func setActive(_ isActive: Bool) { self.isActive = isActive } diff --git a/Sources/AdaRender/Texture/Texture.swift b/Sources/AdaRender/Texture/Texture.swift index ffc305092..4c4d4e985 100644 --- a/Sources/AdaRender/Texture/Texture.swift +++ b/Sources/AdaRender/Texture/Texture.swift @@ -35,11 +35,11 @@ open class Texture: Asset, @unchecked Sendable { return Image() } - public required init(from assetDecoder: any AssetDecoder) throws { + public required init(from assetDecoder: any AssetDecoder) async throws { fatalErrorMethodNotImplemented() } - public func encodeContents(with encoder: any AssetEncoder) throws { + public func encodeContents(with encoder: any AssetEncoder) async throws { fatalErrorMethodNotImplemented() } diff --git a/Sources/AdaRender/Texture/Texture2D.swift b/Sources/AdaRender/Texture/Texture2D.swift index bed74d8f0..add70b275 100644 --- a/Sources/AdaRender/Texture/Texture2D.swift +++ b/Sources/AdaRender/Texture/Texture2D.swift @@ -8,6 +8,11 @@ import AdaAssets import AdaUtils import Math +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif /// The base class represents a 2D texture. /// If the texture isn't held by any object, then the GPU resource will freed immediately. @@ -83,10 +88,10 @@ open class Texture2D: Texture, @unchecked Sendable { /// /// - Parameter decoder: The decoder to initialize the texture from. /// - Throws: An error if the texture cannot be initialized from the decoder. - public convenience required init(from decoder: any AssetDecoder) throws { + public convenience required init(from decoder: any AssetDecoder) async throws { if Self.extensions().contains(where: { $0 == decoder.assetMeta.filePath.pathExtension }) { - let dto = try decoder.decode(TextureSerializable.self) - + let dto = try await decoder.decode(TextureSerializable.self) + let filePath = dto.info?.assetAbsolutePath.path() ?? decoder.assetMeta.filePath.path() let samplerDesc = dto.sampler @@ -96,7 +101,7 @@ open class Texture2D: Texture, @unchecked Sendable { ) self.init(image: image.asset, samplerDescription: samplerDesc) } else { - let image = try Image(from: decoder) + let image = try await Image(from: decoder) self.init(image: image, samplerDescription: image.samplerDescription) } } @@ -105,7 +110,7 @@ open class Texture2D: Texture, @unchecked Sendable { /// /// - Parameter encoder: The encoder to encode the texture to. /// - Throws: An error if the texture cannot be encoded to the encoder. - public override func encodeContents(with encoder: any AssetEncoder) throws { + public override func encodeContents(with encoder: any AssetEncoder) async throws { try encoder.encode( TextureSerializable( info: self.assetMetaInfo, diff --git a/Sources/AdaRender/Texture/TextureAtlas.swift b/Sources/AdaRender/Texture/TextureAtlas.swift index 1c94576bf..2aaaa4c0b 100644 --- a/Sources/AdaRender/Texture/TextureAtlas.swift +++ b/Sources/AdaRender/Texture/TextureAtlas.swift @@ -7,7 +7,11 @@ import AdaAssets import Math +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// The atlas, also know as Sprite Sheet is an object contains an image and can provide /// a little piece of the texture for specific stride. You can describe size of sprite you expect and grab specific sprite by coordinates. @@ -46,8 +50,8 @@ public final class TextureAtlas: Texture2D, @unchecked Sendable { case spriteSize } - public required init(from assetDecoder: any AssetDecoder) throws { - let representation = try assetDecoder.decode(TextureAtlasAssetRepresentation.self) + public required init(from assetDecoder: any AssetDecoder) async throws { + let representation = try await assetDecoder.decode(TextureAtlasAssetRepresentation.self) self.spriteSize = representation.spriteSize self.margin = representation.margin @@ -60,7 +64,7 @@ public final class TextureAtlas: Texture2D, @unchecked Sendable { super.init(image: image, samplerDescription: representation.sampler) } - public override func encodeContents(with assetEncoder: any AssetEncoder) throws { + public override func encodeContents(with assetEncoder: any AssetEncoder) async throws { try assetEncoder.encode( TextureAtlasAssetRepresentation( spriteSize: self.spriteSize, @@ -147,7 +151,7 @@ public extension TextureAtlas { case size } - public convenience required init(from assetDecoder: any AssetDecoder) throws { + public convenience required init(from assetDecoder: any AssetDecoder) async throws { guard let container = try assetDecoder.decoder?.container(keyedBy: CodingKeys.self) else { throw DecodingError.dataCorrupted( DecodingError.Context( @@ -160,11 +164,11 @@ public extension TextureAtlas { let max = try container.decode(Vector2.self, forKey: .max) let size = try container.decode(SizeInt.self, forKey: .size) let textureAtlasDecoder = try container.superDecoder(forKey: .textureAtlasResource) - let textureAtlas = try assetDecoder.decode(TextureAtlas.self, from: textureAtlasDecoder) + let textureAtlas = try await assetDecoder.decode(TextureAtlas.self, from: textureAtlasDecoder) self.init(atlas: textureAtlas, min: min, max: max, size: size) } - public override func encodeContents(with encoder: any AssetEncoder) throws { + public override func encodeContents(with encoder: any AssetEncoder) async throws { if self.atlas.assetPath.isEmpty { throw AssetDecodingError.decodingProblem("Can't encode TextureAtlas.Slice, because TextureAtlas doesn't have resource path on disk.") } @@ -173,7 +177,7 @@ public extension TextureAtlas { throw AssetDecodingError.decodingProblem("Can't encode TextureAtlas.Slice, because not encoder passed") } - try encoder.encode(self.atlas, to: container.superEncoder(forKey: .textureAtlasResource)) + try await encoder.encode(self.atlas, to: container.superEncoder(forKey: .textureAtlasResource)) try container.encode(self.min, forKey: .min) try container.encode(self.max, forKey: .max) try container.encode(SizeInt(width: self.width, height: self.height), forKey: .size) diff --git a/Sources/AdaRender/VisibilityComponents.swift b/Sources/AdaRender/VisibilityComponents.swift index bb86b2a67..c62c6eda7 100644 --- a/Sources/AdaRender/VisibilityComponents.swift +++ b/Sources/AdaRender/VisibilityComponents.swift @@ -6,6 +6,7 @@ // import AdaECS +import AdaUtils /// Contains information about all visible entities on the camera. @Component @@ -27,10 +28,12 @@ public struct VisibleEntities: Sendable { /// This component indicates that entity can be rendered on the screen. /// - Note: By default visibility is always true. @Component -public enum Visibility: Codable, Sendable { +public enum Visibility: Codable, Sendable, DefaultValue { case visible case hidden case inherited + + public static let defaultValue: Self = .visible } /// This components indicates that entity will not affected by frustum culling. diff --git a/Sources/AdaRender/VisibilityPlugin.swift b/Sources/AdaRender/VisibilityPlugin.swift index 4aa257e1f..770706f46 100644 --- a/Sources/AdaRender/VisibilityPlugin.swift +++ b/Sources/AdaRender/VisibilityPlugin.swift @@ -14,6 +14,6 @@ public struct VisibilityPlugin: Plugin { public init() {} public func setup(in app: AppWorlds) { - app.addSystem(VisibilitySystem.self) + app.addSystem(VisibilitySystem.self, on: .preUpdate) } } diff --git a/Sources/AdaRender/VisibleEntities.swift b/Sources/AdaRender/VisibleEntities.swift index 7d3126bad..649c18e0e 100644 --- a/Sources/AdaRender/VisibleEntities.swift +++ b/Sources/AdaRender/VisibleEntities.swift @@ -8,6 +8,7 @@ import AdaECS import AdaTransform import Math +import AdaUtils // TODO: (Vlad) add sphere supports @@ -15,89 +16,43 @@ import Math /// All cameras has frustum and each entity should has ``BoundingComponent`` to be detected. /// If entity doesn't has ``BoundingComponent`` than system tries to add it. /// If entity has ``NoFrustumCulling`` than it will ignore frustum culling. -@System(dependencies: [ +@PlainSystem(dependencies: [ .after(CameraSystem.self) ]) public struct VisibilitySystem { - - @EntityQuery(where: .has(VisibleEntities.self) && .has(Camera.self)) + + @Query> private var cameras - - @EntityQuery( - where: .has(Transform.self) && .has(Visibility.self) - && .has(BoundingComponent.self) && .without(NoFrustumCulling.self) - ) + + @FilterQuery> private var entities - - @EntityQuery( - where: .has(Transform.self) && .has(Visibility.self) && .has(NoFrustumCulling.self) - ) - private var entitiesWithNoFrustum - - @EntityQuery( - where: .has(Transform.self) && .without(Visibility.self) - ) - private var entitiesWithoutVisibility - + public init(world: World) { } - - public func update(context: inout UpdateContext) { - self.addVisibilityIfNeeded() - - self.cameras.forEach { entity in - var (camera, visibleEntities) = entity.components[Camera.self, VisibleEntities.self] - + + public func update(context: UpdateContext) { + self.cameras.forEach { camera, visibleEntities in if !camera.isActive { return } - - let (filtredEntities, entityIds) = self.filterVisibileEntities(context: context, for: camera) - visibleEntities.entities = filtredEntities - visibleEntities.entityIds = entityIds - entity.components[VisibleEntities.self] = visibleEntities - } - } - - private func addVisibilityIfNeeded() { - self.entitiesWithoutVisibility.forEach { entity in - entity.components += Visibility.visible - } - } - - /// Filter entities for passed camera. - private func filterVisibileEntities(context: borrowing UpdateContext, for camera: Camera) -> ([Entity], Set) { - let frustum = camera.computedData.frustum - var entityIds = Set() - let filtredEntities = self.entities.filter { entity in - let (bounding, visibility) = entity.components[BoundingComponent.self, Visibility.self] - - if visibility == .hidden { - return false - } - - switch bounding.bounds { - case .aabb(let aabb): - let isIntersect = frustum.intersectsAABB(aabb) - - if isIntersect { + + let frustum = camera.computedData.frustum + var entityIds = Set() + var entities: [Entity] = [] + self.entities.forEach { entity, visibility, bounding in + if visibility == .hidden { + return + } + switch bounding.bounds { + case .aabb(let aabb): + if !frustum.intersectsAABB(aabb) { + return + } entityIds.insert(entity.id) + entities.append(entity) } - - return isIntersect } + visibleEntities.entities = entities + visibleEntities.entityIds = entityIds } - - let withNoFrustumEntities = self.entitiesWithNoFrustum.filter { entity in - let visibility = entity.components[Visibility.self]! - - if visibility != .hidden { - entityIds.insert(entity.id) - return true - } - - return false - } - let entities = filtredEntities + withNoFrustumEntities - return (entities, entityIds) } } diff --git a/Sources/AdaScene/2D/OrthographicCamera.swift b/Sources/AdaScene/2D/OrthographicCamera.swift index 2271792e7..56977eea1 100644 --- a/Sources/AdaScene/2D/OrthographicCamera.swift +++ b/Sources/AdaScene/2D/OrthographicCamera.swift @@ -10,44 +10,33 @@ import AdaAudio import AdaRender /// A virtual camera that establishes the rendering orthographic. -public final class OrthographicCamera: Entity, @unchecked Sendable { +@Bundle +public struct OrthographicCameraBundle { + public var camera: Camera + public var viewUniforms: GlobalViewUniform + public var globalViewUniformBufferSet: GlobalViewUniformBufferSet + public var visibleEntities: VisibleEntities + public var audioReceiver: AudioReceiver + public var transform: Transform + public var visibility: Visibility - /// A camera component for the orthographic camera entity. - public var camera: Camera { - get { - self.components[Camera.self]! - } - - set { - self.components[Camera.self] = newValue - } - } - - /// Create a new orthograpich camera for rendering 2D and 3D items on screen. - public override init(name: String = "OrthographicCamera") { - super.init(name: name) - - var camera = Camera() - camera.isActive = true - camera.projection = .orthographic - self.components += camera - self.components += VisibleEntities() - self.components += GlobalViewUniform() - self.components += GlobalViewUniformBufferSet() - self.components += AudioReceiver() - self.components += Transform() - } - /// Create a new orthograpich camera for rendering 2D and 3D items on screen. - public init(name: String = "OrthographicCamera", camera: Camera) { - super.init(name: name) - - var camera = camera - camera.isActive = true - camera.projection = .orthographic - self.components += camera - self.components += GlobalViewUniform() - self.components += GlobalViewUniformBufferSet() - self.components += VisibleEntities() + public init( + camera: Camera, + viewUniforms: GlobalViewUniform = GlobalViewUniform(), + globalViewUniformBufferSet: GlobalViewUniformBufferSet = GlobalViewUniformBufferSet(), + visibleEntities: VisibleEntities = VisibleEntities(), + audioReceiver: AudioReceiver = AudioReceiver(), + transform: Transform = Transform(), + visibility: Visibility = .visible + ) { + self.camera = camera + self.camera.projection = .orthographic + self.viewUniforms = viewUniforms + self.globalViewUniformBufferSet = globalViewUniformBufferSet + self.visibleEntities = visibleEntities + self.audioReceiver = audioReceiver + self.transform = transform + self.visibility = visibility } } diff --git a/Sources/AdaScene/3D/PerspectiveCamera.swift b/Sources/AdaScene/3D/PerspectiveCamera.swift index 96271df1a..d5afbde3c 100644 --- a/Sources/AdaScene/3D/PerspectiveCamera.swift +++ b/Sources/AdaScene/3D/PerspectiveCamera.swift @@ -11,47 +11,33 @@ import AdaRender import AdaTransform /// A virtual camera that establishes the rendering perspective. -public final class PerspectiveCamera: Entity, @unchecked Sendable { - - /// A camera component for the perspective camera entity. - public var camera: Camera { - get { - self.components[Camera.self]! - } - - set { - self.components[Camera.self] = newValue - } - } - - /// Create a new perspective camera for rendering 2D and 3D items on screen. - public override init(name: String = "PerspectiveCamera") { - super.init(name: name) - - var camera = Camera() - camera.isActive = true - camera.projection = .perspective - self.components += camera - self.components += VisibleEntities() - self.components += GlobalViewUniform() - self.components += GlobalViewUniformBufferSet() - self.components += AudioReceiver() - self.components += Transform() - self.components += RenderItems() - } - +@Bundle +public struct PerspectiveCameraBundle { + public var camera: Camera + public var visibleEntities = VisibleEntities() + public var globalViewUniform = GlobalViewUniform() + public var globalViewUniformBufferSet = GlobalViewUniformBufferSet() + public var audioReceiver = AudioReceiver() + public var transform = Transform() + public var renderItems = RenderItems() + /// Create a new perspective camera for rendering 2D and 3D items on screen. - public init(name: String = "PerspectiveCamera", camera: Camera) { - super.init(name: name) - - var camera = camera - camera.isActive = true - camera.projection = .perspective - self.components += camera - self.components += GlobalViewUniform() - self.components += GlobalViewUniformBufferSet() - self.components += VisibleEntities() - self.components += Transform() - self.components += RenderItems() + public init( + camera: Camera, + visibleEntities: VisibleEntities = VisibleEntities(), + globalViewUniform: GlobalViewUniform = GlobalViewUniform(), + globalViewUniformBufferSet: GlobalViewUniformBufferSet = GlobalViewUniformBufferSet(), + audioReceiver: AudioReceiver = AudioReceiver(), + transform: Transform = Transform(), + renderItems: RenderItems = RenderItems() + ) { + self.camera = camera + self.camera.projection = .perspective + self.visibleEntities = visibleEntities + self.globalViewUniform = globalViewUniform + self.globalViewUniformBufferSet = globalViewUniformBufferSet + self.audioReceiver = audioReceiver + self.transform = transform + self.renderItems = renderItems } } diff --git a/Sources/AdaScene/Components/ScriptableComponent.swift b/Sources/AdaScene/Components/ScriptableComponent.swift index 098692d87..21fb0240a 100644 --- a/Sources/AdaScene/Components/ScriptableComponent.swift +++ b/Sources/AdaScene/Components/ScriptableComponent.swift @@ -7,9 +7,18 @@ import AdaECS import AdaUtils +import AdaUI import AdaTransform import AdaInput +public struct ScriptableComponent: Component { + public let object: ScriptableObject + + public init(object: ScriptableObject) { + self.object = object + } +} + /// Base class describe some unit of game logic. /// /// - Note: We don't recomend use a lot of scriptable objects, instead use ECS paradigm. @@ -18,7 +27,7 @@ import AdaInput /// /// - Warning: AdaEngine doesn't has execution order for `ScriptableComponent`. /// -open class ScriptableComponent: Component, @unchecked Sendable { +open class ScriptableObject: @unchecked Sendable { internal var isAwaked: Bool = false @@ -62,7 +71,6 @@ open class ScriptableComponent: Component, @unchecked Sendable { public required init(from decoder: Decoder) throws { var mirror: Mirror? = Mirror(reflecting: self) - let container = try decoder.container(keyedBy: CodingName.self) // Go through all mirrors (till top most superclass) @@ -89,9 +97,7 @@ open class ScriptableComponent: Component, @unchecked Sendable { } public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingName.self) - var mirror: Mirror? = Mirror(reflecting: self) // Go through all mirrors (till top most superclass) @@ -117,11 +123,9 @@ open class ScriptableComponent: Component, @unchecked Sendable { mirror = mirror?.superclassMirror } while mirror != nil } - } -public extension ScriptableComponent { - +public extension ScriptableObject { /// Get collection of components in entity /// - Warning: Crashed if component not connected to entity. var components: Entity.ComponentSet { @@ -140,13 +144,12 @@ public extension ScriptableComponent { } /// Set component to entity - func setComponent(_ component: T) { - self.entity?.components.set(component) + func insertComponent(_ component: T) { + self.entity?.components.insert(component) } } -public extension ScriptableComponent { - +public extension ScriptableObject { /// Return transform component for current entity. var transform: Transform { get { diff --git a/Sources/AdaScene/Components/Utils/RequiredComponent.swift b/Sources/AdaScene/Components/Utils/RequiredComponent.swift index 6d8dbdca2..8742c2d2a 100644 --- a/Sources/AdaScene/Components/Utils/RequiredComponent.swift +++ b/Sources/AdaScene/Components/Utils/RequiredComponent.swift @@ -42,7 +42,7 @@ public struct RequiredComponent { // Currently private method to get parent component @MainActor - public static subscript( + public static subscript( _enclosingInstance object: EnclosingSelf, wrapped wrappedKeyPath: KeyPath, storage storageKeyPath: ReferenceWritableKeyPath diff --git a/Sources/AdaScene/GameAppScene.swift b/Sources/AdaScene/GameAppScene.swift index 2a0743bb1..6061e7c31 100644 --- a/Sources/AdaScene/GameAppScene.swift +++ b/Sources/AdaScene/GameAppScene.swift @@ -21,7 +21,7 @@ public struct GameAppScene: AppScene { EmptyWindow() .transformAppWorlds { appWorlds in do { - try appWorlds.addPlugin( + try unsafe appWorlds.addPlugin( GameScenePlugin(gameScene: AssetHandle(gameScene())) ) } catch { @@ -48,8 +48,8 @@ struct GameScenePlugin: Plugin { let gameScene: AssetHandle func setup(in app: AppWorlds) { - let entity = Entity(name: "GameAppScene") - entity.components += DynamicScene(scene: gameScene) - app.mainWorld.addEntity(entity) + app.main.spawn { + DynamicScene(scene: gameScene) + } } } diff --git a/Sources/AdaScene/HotReloading/DynamicScene.swift b/Sources/AdaScene/HotReloading/DynamicScene.swift index 9d9602006..515045154 100644 --- a/Sources/AdaScene/HotReloading/DynamicScene.swift +++ b/Sources/AdaScene/HotReloading/DynamicScene.swift @@ -20,7 +20,7 @@ public struct DynamicScene { /// /// - Parameter world: The world. public init(world: World) { - self.scene = AssetHandle(Scene(from: world)) + self.scene = unsafe AssetHandle(Scene(from: world)) } } @@ -35,7 +35,7 @@ public struct DynamicSceneInstance { } /// A system that initializes and reloads a dynamic scene. -@System +@PlainSystem struct DynamicSceneInitSystem { @Query @@ -43,14 +43,14 @@ struct DynamicSceneInitSystem { init(world: World) { } - func update(context: inout UpdateContext) { - for (entity, scene, instance) in dynamicScenes { + func update(context: UpdateContext) { + dynamicScenes.forEach { (entity, scene, instance) in guard let instance else { insertScene(to: entity, dynamicScene: scene, world: context.world) return } - if instance.identifier != scene.scene.asset.world.id { + if unsafe instance.identifier != scene.scene.asset.world.id { removeChild(from: entity) insertScene(to: entity, dynamicScene: scene, world: context.world) } @@ -58,7 +58,7 @@ struct DynamicSceneInitSystem { } private func insertScene(to rootEntity: Entity, dynamicScene: DynamicScene, world: World) { - let sceneWorld = dynamicScene.scene.asset.world + let sceneWorld = unsafe dynamicScene.scene.asset.world for entity in sceneWorld.getEntities() { let copy = entity.copy() rootEntity.addChild(copy) @@ -75,7 +75,7 @@ struct DynamicSceneInitSystem { private func removeChild(from entity: Entity) { for child in entity.children { - child.removeFromScene(recursively: true) + child.removeFromWorld(recursively: true) } } } diff --git a/Sources/AdaScene/Scene.swift b/Sources/AdaScene/Scene.swift index 0275ca99f..2ba7bddb2 100644 --- a/Sources/AdaScene/Scene.swift +++ b/Sources/AdaScene/Scene.swift @@ -8,7 +8,11 @@ import AdaAssets import AdaECS import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import AdaUI import OrderedCollections @@ -18,10 +22,9 @@ enum SceneSerializationError: Error { case notRegistedObject(String) } -// TODO: (Vlad) MainActor can still in problem. Should we use it? - /// A container that holds the collection of entities for render. -open class Scene: Asset, @unchecked Sendable { +@safe +open class Scene: @unsafe Asset, @unchecked Sendable { public typealias ID = UUID @@ -44,15 +47,6 @@ open class Scene: Asset, @unchecked Sendable { /// Event manager for scene. public private(set) var eventManager: EventManager = EventManager.default - - /// Instance of scene manager which holds this scene. - public internal(set) weak var sceneManager: SceneManager? - - /// Flag indicate that scene is updating right now. - private(set) var isUpdating = false - - /// Check the scene will not run earlier. - private(set) var isReady = false // MARK: - Initialization - @@ -79,7 +73,7 @@ open class Scene: Asset, @unchecked Sendable { let scene = try assetDecoder.decode(SceneSerialization.self) - if Self.currentVersion < scene.version { + if unsafe Self.currentVersion < scene.version { throw SceneSerializationError.unsupportedVersion } @@ -92,7 +86,7 @@ open class Scene: Asset, @unchecked Sendable { throw SceneSerializationError.invalidExtensionType } - try assetEncoder.encode( + unsafe try assetEncoder.encode( SceneSerialization( version: Self.currentVersion, scene: name, @@ -104,65 +98,6 @@ open class Scene: Asset, @unchecked Sendable { public static func extensions() -> [String] { ["ascn", "scene", "scn"] } - - public func update(_ newScene: Scene) async throws { - self.world = newScene.world - self.eventManager = newScene.eventManager - } - - // MARK: - Life Cycle - - /// Tells you when the scene is presented. - /// - /// - Note: Scene is configured and you can't add new systems to the scene. - @MainActor open func sceneDidLoad() { } - - /// Tells you when the scene is about to be removed from a view. - @MainActor open func sceneDidMove(to view: SceneView) { } - - /// Tells you when the scene is about to be removed from a view. - @MainActor open func sceneWillMove(from view: SceneView) { } - - // MARK: - Internal methods - - @MainActor func readyIfNeeded() { - if self.isReady { - return - } - - self.ready() - } - - @MainActor func ready() { - self.isReady = true - self.world.build() - self.eventManager.send(SceneEvents.OnReady(scene: self), source: self) - self.world.insertResource(SceneResource(scene: self)) - self.sceneDidLoad() - } - - /// Update scene world and systems by delta time. - @MainActor func update(_ deltaTime: AdaUtils.TimeInterval) async { - if self.isUpdating { - assertionFailure("Can't update scene twice") - return - } - self.eventManager.send(SceneEvents.Update(scene: self, deltaTime: deltaTime), source: self) - self.isUpdating = true - defer { self.isUpdating = false } - await self.world.update(deltaTime) - } -} - -// MARK: - ECS - -public extension Scene { - - /// Clear all entities from scene - @MainActor - func clearAllEntities() { - return self.world.clear() - } } // MARK: - EventSource diff --git a/Sources/AdaScene/SceneManager.swift b/Sources/AdaScene/SceneManager.swift deleted file mode 100644 index 1b086e234..000000000 --- a/Sources/AdaScene/SceneManager.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// SceneManager.swift -// AdaEngine -// -// Created by v.prusakov on 11/3/21. -// - -import AdaUtils -import AdaUI - -/// SceneManager used for scene managment on screen. Each scene has access to scene manager instance. -/// You can use scene manager for transition between scenes. -@MainActor -public class SceneManager { - - public private(set) var currentScene: Scene? - - /// View where all renders happend - public internal(set) weak var sceneView: SceneView? - - // MARK: - Private - - internal init() { } - - /// Update current scene by delta time. - func update(_ deltaTime: TimeInterval) async { - guard let currentScene else { - return - } - - currentScene.readyIfNeeded() - await currentScene.update(deltaTime) - } - - /// Set window for scene manager. - func setWindow(_ window: UIWindow?) { - self.currentScene?.window = window - } - - // MARK: - Public Methods - - /// Set new scene for presenting on screen. - public func presentScene(_ scene: Scene) { - guard let view = self.sceneView else { - assertionFailure("Can't present scene without SceneView.") - return - } - - self.currentScene?.sceneWillMove(from: view) - scene.sceneManager = self - scene.window = view.window - self.currentScene = scene - scene.sceneDidMove(to: view) - } - -} diff --git a/Sources/AdaScene/Systems/ScriptComponentUpdateSystem.swift b/Sources/AdaScene/Systems/ScriptComponentUpdateSystem.swift index d05f968d7..d257237e3 100644 --- a/Sources/AdaScene/Systems/ScriptComponentUpdateSystem.swift +++ b/Sources/AdaScene/Systems/ScriptComponentUpdateSystem.swift @@ -12,55 +12,55 @@ import AdaUtils import AdaUI /// A system that updates all scripts components on scene -@System(dependencies: [ - .before(CameraSystem.self) -]) +@PlainSystem public struct ScriptComponentUpdateSystem { public init(world: World) { } - public func update(context: inout UpdateContext) { - let scene = context.scene - let world = context.world - let deltaTime = context.deltaTime - context.taskGroup.addTask { @MainActor in - let window = scene?.window - var renderContext: UIGraphicsContext? - - if let window { - renderContext = UIGraphicsContext(window: window) - renderContext?.beginDraw(in: window.frame.size, scaleFactor: 1) - } - - world.getEntities().forEach { entity in - let components = entity.components - .buffer.values.compactMap { $0 as? ScriptableComponent } - - for component in components { - component.entity = entity + @Res + private var deltaTime - // Initialize component - if !component.isAwaked { - component.onReady() - component.isAwaked = true - } - - if let inputManager = world.getResource(Input.self) { - component.onEvent(inputManager.eventsPool) - } - component.onUpdate(deltaTime) + @MainActor + public func update(context: UpdateContext) { + let scene = context.scene +// let world = context.world +// let deltaTime = deltaTime.deltaTime + let window = scene?.window + var renderContext: UIGraphicsContext? -// if fixedTimeResult.isFixedTick { -// component.onPhysicsUpdate(fixedTimeResult.fixedTime) -// } + if let window { +// renderContext = UIGraphicsContext(window: window) +// renderContext?.beginDraw(in: window.frame.size, scaleFactor: 1) + } - if let renderContext { - component.onUpdateGUI(deltaTime, context: renderContext) - } - } - } +// world.getEntities().forEach { entity in +// let components = entity.components +// .buffer.values.compactMap { $0 as? ScriptableComponent } +// +// for component in components { +// component.entity = entity +// +// // Initialize component +// if !component.isAwaked { +// component.onReady() +// component.isAwaked = true +// } +// +// if let inputManager = world.getResource(Input.self) { +// component.onEvent(inputManager.eventsPool) +// } +// component.onUpdate(deltaTime) +// +// // if fixedTimeResult.isFixedTick { +// // component.onPhysicsUpdate(fixedTimeResult.fixedTime) +// // } +// +// if let renderContext { +// component.onUpdateGUI(deltaTime, context: renderContext) +// } +// } +// } - renderContext?.commitDraw() - } + renderContext?.commitDraw() } } diff --git a/Sources/AdaScene/UI/AdaUI+Scene.swift b/Sources/AdaScene/UI/AdaUI+Scene.swift deleted file mode 100644 index 0c586b313..000000000 --- a/Sources/AdaScene/UI/AdaUI+Scene.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// AdaUI+Scene.swift -// AdaEngine -// -// Created by Vladislav Prusakov on 29.05.2025. -// - -import AdaUtils -import AdaUI - -public extension EnvironmentValues { - /// The game scene where view attached. - @Entry internal(set) var scene: WeakBox? -} diff --git a/Sources/AdaScene/UI/SceneView.swift b/Sources/AdaScene/UI/SceneView.swift deleted file mode 100644 index 8386d21fa..000000000 --- a/Sources/AdaScene/UI/SceneView.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// SceneView.swift -// AdaEngine -// -// Created by v.prusakov on 1/9/23. -// - -import AdaUI -import AdaUtils -import Math - -/// This view contains game scene and viewport for rendering. -@MainActor -public class SceneView: UIView { - - /// The scene manager that manage a scenes for this view. - public let sceneManager: SceneManager - - /// A viewport that describe size and depth for rendering. - - public required init(frame: Rect) { - self.sceneManager = SceneManager() - super.init(frame: frame) - self.backgroundColor = .clear - } - - public convenience init(scene: Scene, frame: Rect) { - self.init(frame: frame) - self.sceneManager.presentScene(scene) - } - - public override func frameDidChange() { - super.frameDidChange() - } - - public override func viewWillMove(to window: UIWindow?) { - self.sceneManager.setWindow(window) - } - - public override func update(_ deltaTime: TimeInterval) async { - await self.sceneManager.update(deltaTime) - } -} diff --git a/Sources/AdaSprite/Mesh2d/Mesh2DDrawPass.swift b/Sources/AdaSprite/Mesh2d/Mesh2DDrawPass.swift index 73ce39666..176fa3855 100644 --- a/Sources/AdaSprite/Mesh2d/Mesh2DDrawPass.swift +++ b/Sources/AdaSprite/Mesh2d/Mesh2DDrawPass.swift @@ -5,6 +5,7 @@ // Created by v.prusakov on 4/2/23. // +import AdaECS @_spi(Internal) import AdaRender import Math @@ -15,45 +16,56 @@ struct Mesh2DUniform { /// Contians logic for drawing 2D Meshes. public struct Mesh2DDrawPass: DrawPass { - + public typealias Item = Transparent2DRenderItem + public static let meshUniformBinding: Int = 2 public init() { } - public func render(in context: Context, item: Transparent2DRenderItem) throws { - let meshComponent = item.entity.components[ExctractedMeshPart2d.self]! - + public func render( + with renderEncoder: RenderCommandEncoder, + world: World, + view: Entity, + item: Transparent2DRenderItem + ) throws { + guard let entity = world.getEntityByID(item.entity) else { + return + } + + let meshComponent = entity.components.get(ExctractedMeshPart2d.self) let part = meshComponent.part - let drawList = context.drawList - - guard let materialData = MaterialStorage.shared.getMaterialData(for: meshComponent.material) else { + guard let materialData = unsafe MaterialStorage.shared.getMaterialData(for: meshComponent.material) else { return } - guard let cameraViewUniform = context.view.components[GlobalViewUniformBufferSet.self] else { + guard let cameraViewUniform = view.components[GlobalViewUniformBufferSet.self] else { return } - + + renderEncoder.pushDebugName("Mesh 2D Render") + defer { + renderEncoder.popDebugName() + } + let uniformBuffer = cameraViewUniform.uniformBufferSet.getBuffer( binding: GlobalBufferIndex.viewUniform, set: 0, frameIndex: RenderEngine.shared.currentFrameIndex ) - drawList.appendUniformBuffer(uniformBuffer, for: .vertex) - - drawList.pushDebugName("Mesh 2D Render") + renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, index: GlobalBufferIndex.viewUniform) + for (uniformName, buffer) in materialData.reflectionData.shaderBuffers { guard let uniformBuffer = materialData.uniformBufferSet[uniformName]?.getBuffer(binding: buffer.binding, set: 0, frameIndex: RenderEngine.shared.currentFrameIndex) else { continue } if buffer.shaderStage.contains(.vertex) { - drawList.appendUniformBuffer(uniformBuffer, for: .vertex) + renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, index: buffer.binding) } if buffer.shaderStage.contains(.fragment) { - drawList.appendUniformBuffer(uniformBuffer, for: .fragment) + renderEncoder.setFragmentBuffer(uniformBuffer, offset: 0, index: buffer.binding) } } @@ -63,23 +75,20 @@ public struct Mesh2DDrawPass: DrawPass { } for texture in textures { - drawList.bindTexture(texture, at: resource.binding) + renderEncoder.setFragmentTexture(texture, index: resource.binding) + renderEncoder.setFragmentSamplerState(texture.sampler, index: resource.binding) } } - let meshUniformBuffer = context.device.createUniformBuffer(Mesh2DUniform.self, binding: Self.meshUniformBinding) - meshUniformBuffer.setData(meshComponent.modelUniform) - - meshUniformBuffer.setData(meshComponent.modelUniform) - - drawList.appendUniformBuffer(meshUniformBuffer, for: .vertex) + unsafe withUnsafeBytes(of: meshComponent.modelUniform) { buffer in + unsafe renderEncoder.setVertexBytes(buffer.baseAddress!, length: buffer.count, index: Self.meshUniformBinding) + } - drawList.appendVertexBuffer(part.vertexBuffer) - drawList.bindIndexBuffer(part.indexBuffer) - drawList.bindIndexPrimitive(part.primitiveTopology.indexPrimitive) - drawList.bindRenderPipeline(item.renderPipeline) + renderEncoder.setVertexBuffer(part.vertexBuffer, offset: 0, index: 0) + renderEncoder.setIndexBuffer(part.indexBuffer, offset: 0) + renderEncoder.setRenderPipelineState(item.renderPipeline) - drawList.drawIndexed(indexCount: part.indexCount, instanceCount: 1) + renderEncoder.drawIndexed(indexCount: part.indexCount, indexBufferOffset: 0, instanceCount: 1) } } diff --git a/Sources/AdaSprite/Mesh2d/Mesh2DSystem.swift b/Sources/AdaSprite/Mesh2d/Mesh2DSystem.swift index 4b1ccdec8..21f73ddf1 100644 --- a/Sources/AdaSprite/Mesh2d/Mesh2DSystem.swift +++ b/Sources/AdaSprite/Mesh2d/Mesh2DSystem.swift @@ -8,8 +8,9 @@ import AdaApp import AdaECS import AdaTransform -import Math +import AdaUtils @_spi(Internal) import AdaRender +import Math // MARK: - Mesh 2D Plugin - @@ -23,8 +24,18 @@ public struct Mesh2DPlugin: Plugin { /// /// - Parameter app: The app. public func setup(in app: AppWorlds) { + Mesh2DComponent.registerComponent() + + app.main.registerRequiredComponent(Visibility.self, for: Mesh2DComponent.self) + app.main.registerRequiredComponent(BoundingComponent.self, for: Mesh2DComponent.self) + let renderWorld = app.getSubworldBuilder(by: .renderWorld) - renderWorld?.addSystem(ExctractMesh2DSystem.self) + .unwrap(message: "RenderWorld not found") + renderWorld + .insertResource(ExctractedMeshes2D()) + .insertResource(Mesh2DDrawPass()) + .addSystem(ExctractMesh2DSystem.self, on: .extract) + .addSystem(Mesh2DRenderSystem.self, on: .update) } } @@ -62,18 +73,20 @@ public struct ExctractedMesh2D: Sendable { } /// System to render exctract meshes to RenderWorld. -@System +@PlainSystem public struct ExctractMesh2DSystem { - @Extract< Query > private var query + @ResMut + private var extractedMeshes + public init(world: World) { } - public func update(context: inout UpdateContext) { - var extractedMeshes = ExctractedMeshes2D() + public func update(context: UpdateContext) { + extractedMeshes.meshes.removeAll(keepingCapacity: true) self.query.wrappedValue.forEach { entity, mesh, transform, globalTransform, visibility in if visibility == .hidden { return @@ -88,62 +101,50 @@ public struct ExctractMesh2DSystem { ) ) } - context.world.insertResource(extractedMeshes) } } // MARK: - Mesh 2D Render Plugin - -/// Plugin for RenderWorld for rendering 2D meshes. -public struct Mesh2DRenderPlugin: Plugin { - - public init() {} - - public func setup(in app: AppWorlds) { - Mesh2DComponent.registerComponent() - - guard let renderWorld = app.getSubworldBuilder(by: .renderWorld) else { - return - } - renderWorld - .insertResource(Mesh2DDrawPass()) - .addSystem(Mesh2DRenderSystem.self) - } -} - /// System in RenderWorld for rendering 2D meshes. -@System +@PlainSystem public struct Mesh2DRenderSystem: Sendable { @Query>> private var query - @ResQuery - private var extractedMeshes: ExctractedMeshes2D! + @Res + private var extractedMeshes + + @Res + private var meshDrawPass + + @Res + private var renderDevice - @ResQuery - private var meshDrawPass: Mesh2DDrawPass! + @Commands + private var commands public init(world: World) { } - public func update(context: inout UpdateContext) { + public func update(context: UpdateContext) { self.query.forEach { visibleEntities, renderItems in self.draw( - meshes: extractedMeshes.meshes, + world: context.world, visibleEntities: visibleEntities, - items: &renderItems.items, + items: renderItems, keys: [] ) } } func draw( - meshes: [ExctractedMesh2D], + world: World, visibleEntities: VisibleEntities, - items: inout [Transparent2DRenderItem], + items: Ref>, keys: Set ) { - for mesh in meshes { + for mesh in extractedMeshes.meshes { guard visibleEntities.entityIds.contains(mesh.entityId) else { continue } @@ -157,22 +158,27 @@ public struct Mesh2DRenderSystem: Sendable { for part in model.parts { let material = mesh.mesh.materials[part.materialIndex] - guard let pipeline = material.getOrCreatePipeline(for: part.vertexDescriptor, keys: keys) else { + guard let pipeline = material.getOrCreatePipeline( + for: part.vertexDescriptor, + keys: keys, + device: renderDevice.renderDevice + ) else { assertionFailure("No render pipeline for mesh") continue } - let emptyEntity = Entity() - emptyEntity.components += ExctractedMeshPart2d( - part: part, - material: material, - modelUniform: modelUniform - ) + let entity = commands.spawn() { + ExctractedMeshPart2d( + part: part, + material: material, + modelUniform: modelUniform + ) + }.entityId - items.append( + items.items.append( Transparent2DRenderItem( - entity: emptyEntity, - batchEntity: emptyEntity, + entity: entity, + batchEntity: entity, drawPass: self.meshDrawPass, renderPipeline: pipeline, sortKey: mesh.transform.position.z @@ -203,7 +209,6 @@ public class Mesh2dMaterialStorageData: MaterialStorageData { // TODO: Think about it, maybe we should move it to other dir. extension Material { - /// Get Mesh2D material key which has been used for caching. func getMesh2dMaterialKey(for vertexDescritor: VertexDescriptor, keys: Set) -> MaterialMesh2dKey { let defines = self.collectDefines(for: vertexDescritor, keys: keys) @@ -211,15 +216,19 @@ extension Material { } /// Get or create Mesh2D Material render pipeline from vertex and keys. - func getOrCreatePipeline(for vertexDescriptor: VertexDescriptor, keys: Set) -> RenderPipeline? { + func getOrCreatePipeline( + for vertexDescriptor: VertexDescriptor, + keys: Set, + device: RenderDevice + ) -> RenderPipeline? { let materialKey = self.getMesh2dMaterialKey(for: vertexDescriptor, keys: keys) - if let data = MaterialStorage.shared.getMaterialData(for: self) as? Mesh2dMaterialStorageData { + if let data = unsafe MaterialStorage.shared.getMaterialData(for: self) as? Mesh2dMaterialStorageData { if let pipeline = data.pipelines[materialKey] { return pipeline } - guard let (pipeline, shaderModule) = self.createPipeline(for: materialKey) else { + guard let (pipeline, shaderModule) = self.createPipeline(for: materialKey, device: device) else { return nil } @@ -230,14 +239,14 @@ extension Material { return pipeline } else { - guard let (pipeline, shaderModule) = self.createPipeline(for: materialKey) else { + guard let (pipeline, shaderModule) = self.createPipeline(for: materialKey, device: device) else { return nil } let data = Mesh2dMaterialStorageData() data.updateUniformBuffers(from: shaderModule) data.pipelines[materialKey] = pipeline - MaterialStorage.shared.setMaterialData(data, for: self) + unsafe MaterialStorage.shared.setMaterialData(data, for: self) self.update() @@ -245,7 +254,10 @@ extension Material { } } - private func createPipeline(for materialKey: MaterialMesh2dKey) -> (RenderPipeline, ShaderModule)? { + private func createPipeline( + for materialKey: MaterialMesh2dKey, + device: RenderDevice + ) -> (RenderPipeline, ShaderModule)? { let compiler = ShaderCompiler(shaderSource: self.shaderSource) for define in materialKey.defines { @@ -260,9 +272,9 @@ extension Material { return nil } - return (RenderEngine.shared.renderDevice.createRenderPipeline(from: pipelineDesc), shaderModule) + return (device.createRenderPipeline(from: pipelineDesc), shaderModule) } catch { - assertionFailure("[Mesh2DRenderSystem] \(error.localizedDescription)") + assertionFailure("[Mesh2DRenderSystem] \(error)") return nil } } diff --git a/Sources/AdaSprite/SpriteDrawPass.swift b/Sources/AdaSprite/SpriteDrawPass.swift index c747f9951..b2b8ebac0 100644 --- a/Sources/AdaSprite/SpriteDrawPass.swift +++ b/Sources/AdaSprite/SpriteDrawPass.swift @@ -5,6 +5,7 @@ // Created by v.prusakov on 2/23/23. // +import AdaECS import AdaRender import AdaUtils import Math @@ -18,39 +19,43 @@ struct SpriteVertexData { /// Render draw pass for rendering sprites. Support batching. public struct SpriteDrawPass: DrawPass { - public func render(in context: Context, item: Transparent2DRenderItem) throws { - guard let spriteData = context.entity.components[SpriteDataComponent.self] else { + public func render( + with renderEncoder: RenderCommandEncoder, + world: World, + view: Entity, + item: Transparent2DRenderItem + ) throws { + guard + let cameraViewUniform = view.components[GlobalViewUniformBufferSet.self], + let spritesData = world.getResource(SpriteDrawData.self) + else { return } - - guard let cameraViewUniform = context.view.components[GlobalViewUniformBufferSet.self] else { - return + + renderEncoder.pushDebugName("SpriteDrawPass") + defer { + renderEncoder.popDebugName() } - - context.drawList.pushDebugName("SpriteDrawPass") - let uniformBuffer = cameraViewUniform.uniformBufferSet.getBuffer( binding: GlobalBufferIndex.viewUniform, set: 0, frameIndex: RenderEngine.shared.currentFrameIndex ) - - if let batchSprite = item.batchEntity.components[BatchComponent.self] { - batchSprite.textures.enumerated().forEach { (index, texture) in - context.drawList.bindTexture(texture, at: index) - } + guard let batchSprite = world.getEntityByID(item.batchEntity)?.components[TextureBatchComponent.self] else { + return + } + batchSprite.textures.enumerated().forEach { (index, texture) in + renderEncoder.setFragmentTexture(texture, index: index) + renderEncoder.setFragmentSamplerState(texture.sampler, index: index) } - - context.drawList.appendUniformBuffer(uniformBuffer) - context.drawList.appendVertexBuffer(spriteData.vertexBuffer) - context.drawList.bindIndexBuffer(spriteData.indexBuffer) - context.drawList.bindRenderPipeline(item.renderPipeline) - context.drawList.drawIndexed( + renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, index: GlobalBufferIndex.viewUniform) + renderEncoder.setVertexBuffer(spritesData.vertexBuffer, offset: 0, index: 0) + renderEncoder.setIndexBuffer(spritesData.indexBuffer, indexFormat: .uInt32) + renderEncoder.setRenderPipelineState(item.renderPipeline) + renderEncoder.drawIndexed( indexCount: item.batchRange?.count ?? 6, // indicies count per quad indexBufferOffset: Int(item.batchRange?.lowerBound ?? 0) * 4, // start position must be multiple by 4 instanceCount: 1 ) - - context.drawList.popDebugName() } } diff --git a/Sources/AdaSprite/SpritePlugin.swift b/Sources/AdaSprite/SpritePlugin.swift index db8f34227..a52ada144 100644 --- a/Sources/AdaSprite/SpritePlugin.swift +++ b/Sources/AdaSprite/SpritePlugin.swift @@ -19,18 +19,19 @@ public struct SpritePlugin: Plugin { app .addSystem(UpdateBoundingsSystem.self, on: .postUpdate) + .main + .registerRequiredComponent(Visibility.self, for: SpriteComponent.self) + .registerRequiredComponent(BoundingComponent.self, for: SpriteComponent.self) guard let renderWorld = app.getSubworldBuilder(by: .renderWorld) else { return } - let pipeline = SpriteRenderPipeline() - renderWorld - .insertResource(pipeline) + .insertResource(ExtractedSprites()) + .createResource(RenderPipelines.self) .insertResource(SpriteDrawPass()) - .addSystem(ExtractSpriteSystem.self) - .addSystem(SpriteRenderSystem.self) - .addSystem(ExctractMesh2DSystem.self) + .addSystem(ExtractSpriteSystem.self, on: .extract) + .addSystem(SpriteRenderSystem.self, on: .update) } } diff --git a/Sources/AdaSprite/SpriteRenderPipeline.swift b/Sources/AdaSprite/SpriteRenderPipeline.swift index d3b079e02..73ee9b3ad 100644 --- a/Sources/AdaSprite/SpriteRenderPipeline.swift +++ b/Sources/AdaSprite/SpriteRenderPipeline.swift @@ -8,19 +8,27 @@ import AdaECS import AdaAssets import AdaRender -import Foundation -struct SpriteRenderPipeline: Resource { - let renderPipeline: RenderPipeline +public struct SpriteRenderPipeline: RenderPipelineConfigurator { + public let spriteShader: AssetHandle - init() { - let device = RenderEngine.shared.renderDevice - let spriteShader = try! AssetsManager.loadSync( + public init() { + self.spriteShader = try! AssetsManager.loadSync( ShaderModule.self, at: "Assets/sprite.glsl", from: .module ) + } +} + +extension SpriteRenderPipeline: WorldInitable { + public init(from world: World) { + self = Self.init() + } +} +extension SpriteRenderPipeline { + public func configurate(with configuration: RenderPipelineEmptyConfiguration) -> RenderPipelineDescriptor { var piplineDesc = RenderPipelineDescriptor() piplineDesc.vertex = spriteShader.asset.getShader(for: .vertex) piplineDesc.fragment = spriteShader.asset.getShader(for: .fragment) @@ -33,8 +41,7 @@ struct SpriteRenderPipeline: Resource { ]) piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride - piplineDesc.colorAttachments = [ColorAttachmentDescriptor(format: .bgra8, isBlendingEnabled: true)] - let quadPipeline = device.createRenderPipeline(from: piplineDesc) - self.renderPipeline = quadPipeline + piplineDesc.colorAttachments = [RenderPipelineColorAttachmentDescriptor(format: .bgra8, isBlendingEnabled: true)] + return piplineDesc } } diff --git a/Sources/AdaSprite/SpriteRenderSystem.swift b/Sources/AdaSprite/SpriteRenderSystem.swift index 888fa5763..815a29364 100644 --- a/Sources/AdaSprite/SpriteRenderSystem.swift +++ b/Sources/AdaSprite/SpriteRenderSystem.swift @@ -10,15 +10,112 @@ import AdaRender import AdaUtils import AdaTransform import Math +import AdaAssets -// TODO: Rewrite sprite batch if needed. Too much drawcalls, I think +@Component +/// A component that contains the textures for a batch. +public struct TextureBatchComponent { + /// The textures for a batch. + public var textures: [Texture2D] +} + +// MARK: Extraction to Render World + +/// A resource that contains the extracted sprites. +public struct ExtractedSprites: Resource { + /// The extracted sprites. + public var sprites: [ExtractedSprite] + + /// Initialize a new extracted sprites. + /// + /// - Parameter sprites: The extracted sprites. + public init(sprites: [ExtractedSprite] = []) { + self.sprites = sprites + } +} + +/// A sprite that contains the extracted sprite. +public struct ExtractedSprite: Sendable { + /// The entity id of the extracted sprite. + public var entityId: Entity.ID + /// The texture of the extracted sprite. + public var texture: Texture2D? + /// The flip x of the extracted sprite. + public var flipX: Bool + /// The flip y of the extracted sprite. + public var flipY: Bool + /// The tint color of the extracted sprite. + public var tintColor: Color + /// The transform of the extracted sprite. + public var transform: Transform + /// The world transform of the extracted sprite. + public var worldTransform: Transform3D +} +/// A data for drawing sprites. +public struct SpriteDrawData: Resource { + let vertexBuffer: BufferData + let indexBuffer: BufferData +} + +/// Exctract sprites to RenderWorld for future rendering. @System(dependencies: [ - .after(ExtractCameraSystem.self), - .after(BatchTransparent2DItemsSystem.self) + .before(SpriteRenderSystem.self) ]) -public struct SpriteRenderSystem: Sendable { +@inline(__always) +public func ExtractSprite( + _ world: World, + _ sprites: Extract< + Query + >, + _ extractedSprites: ResMut +) { + extractedSprites.sprites.removeAll(keepingCapacity: true) + sprites.wrappedValue.forEach { entity, sprite, globalTransform, transform, visible in + if visible == .hidden { + return + } + extractedSprites.sprites.append( + ExtractedSprite( + entityId: entity.id, + texture: sprite.texture?.asset, + flipX: sprite.flipX, + flipY: sprite.flipY, + tintColor: sprite.tintColor, + transform: transform, + worldTransform: globalTransform.matrix + ) + ) + } +} +@System +@inline(__always) +func UpdateBoundings( + _ sprites: FilterQuery< + Entity, Transform, Ref, + And, Changed>, + >, + _ meshes: FilterQuery< + Mesh2DComponent, Ref, + Changed + > +) async { + await sprites.parallel().forEach { entity, transform, bounds in + let position = transform.position + let scale = transform.scale + let min = Vector3(position.x - scale.x / 2, position.y - scale.y / 2, 0) + let max = Vector3(position.x + scale.x / 2, position.y + scale.y / 2, 0) + bounds.bounds = .aabb(AABB(min: min, max: max)) + } + + await meshes.parallel().forEach { mesh2d, bounds in + bounds.bounds = .aabb(mesh2d.mesh.bounds) + } +} + +@PlainSystem +public struct SpriteRenderSystem: Sendable { @Query< Camera, VisibleEntities, @@ -26,14 +123,20 @@ public struct SpriteRenderSystem: Sendable { > private var cameras - @ResQuery + @Res private var extractedSprites: ExtractedSprites? - @ResQuery - private var spriteDrawPass: SpriteDrawPass! + @Res + private var spriteDrawPass: SpriteDrawPass + + @ResMut + private var spriteRenderPipeline: RenderPipelines - @ResQuery - private var spriteRenderPipeline: SpriteRenderPipeline! + @Res + private var renderDevice: RenderDeviceHandler + + @Commands + private var commands static let quadPosition: [Vector4] = [ [-0.5, -0.5, 0.0, 1.0], @@ -46,12 +149,13 @@ public struct SpriteRenderSystem: Sendable { public init(world: World) { } - public func update(context: inout UpdateContext) { - for (_, visibleEntities, renderItems) in cameras { + public func update(context: UpdateContext) { + cameras.forEach { (_, visibleEntities, renderItems) in self.draw( + world: context.world, extractedSprites: self.extractedSprites?.sprites ?? [], visibleEntities: visibleEntities, - renderItems: &renderItems.wrappedValue + renderItems: renderItems ) } } @@ -60,26 +164,25 @@ public struct SpriteRenderSystem: Sendable { // swiftlint:disable:next function_body_length private func draw( + world: World, extractedSprites: [ExtractedSprite], visibleEntities: VisibleEntities, - renderItems: inout RenderItems + renderItems: Ref> ) { - let spriteData = Entity(name: "sprite_data") - + let device = renderDevice.renderDevice + let spriteData = commands.spawn("sprite_data") let sprites = extractedSprites .sorted { lhs, rhs in lhs.transform.position.z < rhs.transform.position.z } - var spriteVerticies = [SpriteVertexData]() - spriteVerticies.reserveCapacity(MemoryLayout.stride * sprites.count) + var spriteVerticies: BufferData = [] + spriteVerticies.label = "SpriteRenderSystem_VertexBuffer" var indeciesCount: Int32 = 0 - var textureSlotIndex = 1 - - var currentBatchEntity = Entity() - var currentBatch = BatchComponent( + var currentBatchEntity = commands.spawn("Batch entity") + var currentBatch = TextureBatchComponent( textures: [Texture2D].init(repeating: .whiteTexture, count: Self.maxTexturesPerBatch) ) @@ -89,12 +192,11 @@ public struct SpriteRenderSystem: Sendable { } let worldTransform = sprite.worldTransform - if textureSlotIndex >= Self.maxTexturesPerBatch { - currentBatchEntity.components += currentBatch + currentBatchEntity.insert(currentBatch) textureSlotIndex = 1 - currentBatchEntity = Entity() - currentBatch = BatchComponent( + currentBatchEntity = commands.spawn("Batch entity") + currentBatch = TextureBatchComponent( textures: [Texture2D].init(repeating: .whiteTexture, count: Self.maxTexturesPerBatch) ) } @@ -131,34 +233,28 @@ public struct SpriteRenderSystem: Sendable { indeciesCount += 6 let itemEnd = indeciesCount + let pipeline = spriteRenderPipeline.pipeline(device: device) renderItems.items.append( Transparent2DRenderItem( - entity: spriteData, - batchEntity: currentBatchEntity, + entity: spriteData.entityId, + batchEntity: currentBatchEntity.entityId, drawPass: self.spriteDrawPass, - renderPipeline: self.spriteRenderPipeline.renderPipeline, + renderPipeline: pipeline, sortKey: sprite.transform.position.z, batchRange: itemStart...stride, - binding: 0 - ) - vertexBuffer.label = "SpriteRenderSystem_VertexBuffer" - let indicies = Int(indeciesCount * 4) - - var quadIndices = [UInt32].init(repeating: 0, count: indicies) + var quadIndices: BufferData = .init(elements: [UInt32].init(repeating: 0, count: indicies)) + quadIndices.label = "SpriteRenderSystem_IndexBuffer" var offset: UInt32 = 0 for index in stride(from: 0, to: indicies, by: 6) { @@ -173,123 +269,14 @@ public struct SpriteRenderSystem: Sendable { offset += 4 } - vertexBuffer.setData(&spriteVerticies, byteCount: spriteVerticies.count * MemoryLayout.stride) + spriteVerticies.write(to: device) + quadIndices.write(to: device) - var quadIndexBuffer = device.createIndexBuffer( - format: .uInt32, - bytes: &quadIndices, - length: indicies - ) - quadIndexBuffer.label = "SpriteRenderSystem_IndexBuffer" - - spriteData.components += SpriteDataComponent( - vertexBuffer: vertexBuffer, - indexBuffer: quadIndexBuffer - ) - } -} - -@Component -struct SpriteDataComponent { - let vertexBuffer: VertexBuffer - let indexBuffer: IndexBuffer -} - -@Component -/// A component that contains the textures for a batch. -public struct BatchComponent { - /// The textures for a batch. - public var textures: [Texture2D] -} - -// MARK: Extraction to Render World - -/// A resource that contains the extracted sprites. -public struct ExtractedSprites: Resource { - /// The extracted sprites. - public var sprites: [ExtractedSprite] - - /// Initialize a new extracted sprites. - /// - /// - Parameter sprites: The extracted sprites. - public init(sprites: [ExtractedSprite]) { - self.sprites = sprites - } -} - -/// A sprite that contains the extracted sprite. -public struct ExtractedSprite: Sendable { - /// The entity id of the extracted sprite. - public var entityId: Entity.ID - /// The texture of the extracted sprite. - public var texture: Texture2D? - /// The flip x of the extracted sprite. - public var flipX: Bool - /// The flip y of the extracted sprite. - public var flipY: Bool - /// The tint color of the extracted sprite. - public var tintColor: Color - /// The transform of the extracted sprite. - public var transform: Transform - /// The world transform of the extracted sprite. - public var worldTransform: Transform3D -} - -/// Exctract sprites to RenderWorld for future rendering. -@PlainSystem(dependencies: [ - .before(SpriteRenderSystem.self) -]) -public func ExtractSprite( - _ world: Ref, - _ sprites: Extract> -) { - var extractedSprites = ExtractedSprites(sprites: []) - sprites().wrappedValue.forEach { entity, sprite, globalTransform, transform, visible in - if visible == .hidden { - return - } - - extractedSprites.sprites.append( - ExtractedSprite( - entityId: entity.id, - texture: sprite.texture?.asset, - flipX: sprite.flipX, - flipY: sprite.flipY, - tintColor: sprite.tintColor, - transform: transform, - worldTransform: globalTransform.matrix + commands.insertResource( + SpriteDrawData( + vertexBuffer: spriteVerticies, + indexBuffer: quadIndices ) ) } - world.wrappedValue.insertResource(extractedSprites) -} - -@PlainSystem -func UpdateBoundings( - _ entitiesWithTransform: Query -) { - entitiesWithTransform().forEach { entity, transform in - var bounds: BoundingComponent.Bounds? - - if entity.components.has(SpriteComponent.self) { - if !entity.components.isComponentChanged(Transform.self) && entity.components.has(BoundingComponent.self) { - return - } - - let transform = entity.components[Transform.self]! - let position = transform.position - let scale = transform.scale - - let min = Vector3(position.x - scale.x / 2, position.y - scale.y / 2, 0) - let max = Vector3(position.x + scale.x / 2, position.y + scale.y / 2, 0) - - bounds = .aabb(AABB(min: min, max: max)) - } else if let mesh2d = entity.components[Mesh2DComponent.self] { - bounds = .aabb(mesh2d.mesh.bounds) - } - - if let bounds { - entity.components += BoundingComponent(bounds: bounds) - } - } } diff --git a/Sources/AdaSprite/Text2DRenderSystem.swift b/Sources/AdaSprite/Text2DRenderSystem.swift new file mode 100644 index 000000000..38580886a --- /dev/null +++ b/Sources/AdaSprite/Text2DRenderSystem.swift @@ -0,0 +1,167 @@ +// +// Text2DRenderSystem.swift +// AdaEngine +// +// Created by v.prusakov on 3/7/23. +// + +import AdaAssets +import AdaECS +import AdaRender +import AdaTransform +import AdaText +import Math + +@PlainSystem +public struct Text2DRenderSystem { + + static let quadPosition: [Vector4] = [ + [-0.5, -0.5, 0.0, 1.0], + [ 0.5, -0.5, 0.0, 1.0], + [ 0.5, 0.5, 0.0, 1.0], + [-0.5, 0.5, 0.0, 1.0] + ] + + static let maxTexturesPerBatch = 16 + + @Query + private var textComponents + + @Query>> + private var cameras + + @Res + private var spriteDraw: SpriteDrawPass + + @Commands + private var commands + + @ResMut + private var pipelines: RenderPipelines + + @Res + private var renderDevice: RenderDeviceHandler + + public init(world: World) {} + + public func update(context: UpdateContext) async { + self.cameras.forEach { visibleEntities, renderItems in + self.draw( + world: context.world, + visibleEntities: visibleEntities.entities, + renderItems: renderItems + ) + } + } + + // swiftlint:disable:next function_body_length + private func draw( + world: World, + visibleEntities: [Entity], + renderItems: Ref> + ) { + let texts = visibleEntities.filter { + $0.components.has(TextComponent.self) && $0.components.has(TextLayoutComponent.self) + } + .sorted { lhs, rhs in + lhs.components[Transform.self]!.position.z < rhs.components[Transform.self]!.position.z + } + + for entity in texts { + guard let textLayout = entity.components[TextLayoutComponent.self] else { + continue + } + + let currentBatchEntity = commands.spawn() + let transform = entity.components[Transform.self]! + let worldTransform = entity.components[GlobalTransform.self]!.matrix + let glyphs = textLayout.textLayout.getGlyphVertexData(transform: worldTransform) + + var spriteVerticies = glyphs.verticies + + if spriteVerticies.isEmpty { + continue + } + + // TODO: Redesign it latter + var textures: [Texture2D] = [Texture2D].init(repeating: .whiteTexture, count: Self.maxTexturesPerBatch) + glyphs.textures.compactMap { $0 }.enumerated().forEach { index, texture in + textures[index] = texture + } + +// currentBatchEntity.components += TextureBatchComponent(textures: textures) + +// renderItems.items.append( +// Transparent2DRenderItem( +// entity: currentBatchEntity, +// batchEntity: currentBatchEntity, +// drawPassId: spriteDraw, +// renderPipeline: self.textRenderPipeline, +// sortKey: transform.position.z, +// batchRange: 0...stride, +// binding: 0 +// ) +// vertexBuffer.label = "Text2DRenderSystem_VertexBuffer" + + let indicies = Int(glyphs.indeciesCount * 4) + + var quadIndices = [UInt32].init(repeating: 0, count: indicies) + + var offset: UInt32 = 0 + for index in stride(from: 0, to: indicies, by: 6) { + quadIndices[index + 0] = offset + 0 + quadIndices[index + 1] = offset + 1 + quadIndices[index + 2] = offset + 2 + + quadIndices[index + 3] = offset + 2 + quadIndices[index + 4] = offset + 3 + quadIndices[index + 5] = offset + 0 + + offset += 4 + } + +// vertexBuffer.setData(&spriteVerticies, byteCount: spriteVerticies.count * MemoryLayout.stride) + +// let quadIndexBuffer = device.createIndexBuffer( +// format: .uInt32, +// bytes: &quadIndices, +// length: indicies +// ) +// quadIndexBuffer.label = "Text2DRenderSystem_IndexBuffer" +// +// currentBatchEntity.components += SpriteDataComponent( +// vertexBuffer: vertexBuffer, +// indexBuffer: quadIndexBuffer +// ) + } + } +} + +@PlainSystem(dependencies: [ + .after(TextLayoutSystem.self) +]) +struct ExctractTextSystem { + + @Query + private var textComponents + + @ResMut + private var extractedSprites: ExtractedSprites + + init(world: World) { } + + func update(context: UpdateContext) { + self.textComponents.forEach { textComponent, textLayoutComponent, transform in + + } + } +} + +public struct ExctractedText { + +} diff --git a/Sources/AdaSprite/Text2DSystem.swift b/Sources/AdaSprite/Text2DSystem.swift new file mode 100644 index 000000000..cf424b336 --- /dev/null +++ b/Sources/AdaSprite/Text2DSystem.swift @@ -0,0 +1,32 @@ +// +// Text2DSystem.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 05.12.2025. +// + +import AdaECS +import AdaTransform +import AdaText +import AdaRender +import Math + +@System +@inline(__always) +func Text2DUpdateBoundings( + _ texts: FilterQuery< + Transform, + TextLayoutComponent, + Ref, + Changed + > +) async { + await texts.parallel().forEach { transform, layout, bounds in +// layout.textLayout.size + let position = transform.position + let scale = transform.scale + let min = Vector3(position.x - scale.x / 2, position.y - scale.y / 2, 0) + let max = Vector3(position.x + scale.x / 2, position.y + scale.y / 2, 0) + bounds.bounds = .aabb(AABB(min: min, max: max)) + } +} diff --git a/Sources/AdaText/Text/Font/FontAtlasGenerator.swift b/Sources/AdaText/Text/Font/FontAtlasGenerator.swift index 5946cb36c..e9a757b37 100644 --- a/Sources/AdaText/Text/Font/FontAtlasGenerator.swift +++ b/Sources/AdaText/Text/Font/FontAtlasGenerator.swift @@ -8,7 +8,11 @@ import AdaRender import AdaUtils import AtlasFontGenerator +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// A font descriptor. public struct FontDescriptor { @@ -17,9 +21,9 @@ public struct FontDescriptor { } /// Generate MTSDF atlas texture from font. -final class FontAtlasGenerator { - - nonisolated(unsafe) static let shared = FontAtlasGenerator() +final class FontAtlasGenerator: Sendable { + + static let shared = FontAtlasGenerator() private init() {} @@ -42,42 +46,42 @@ final class FontAtlasGenerator { let fontPathString = fontPath.path let fontName = fontPath.lastPathComponent - let generator = fontPathString.withCString { fontPathPtr in - fontName.withCString { fontNamePtr in - font_atlas_generator_create(fontPathPtr, fontNamePtr, atlasFontDescriptor)! + let generator = unsafe fontPathString.withCString { fontPathPtr in + unsafe fontName.withCString { fontNamePtr in + unsafe font_atlas_generator_create(fontPathPtr, fontNamePtr, atlasFontDescriptor)! } } defer { - generator.deallocate() + unsafe generator.deallocate() } - let fontData = font_atlas_generator_get_font_data(generator)! + let fontData = unsafe font_atlas_generator_get_font_data(generator)! let fileName = "\(fontName)-\(atlasFontDescriptor.emFontScale.rounded()).fontbin" if let (atlasHeader, data) = self.getAtlas(by: fileName) { let texture = self.makeTextureAtlas(from: data, width: atlasHeader.width, height: atlasHeader.height) - return FontHandle(atlasTexture: texture, fontData: fontData) + return unsafe FontHandle(atlasTexture: texture, fontData: fontData) } else { - let bitmap = font_atlas_generator_generate_bitmap(generator)! - + let bitmap = unsafe font_atlas_generator_generate_bitmap(generator)! + defer { - bitmap.deallocate() + unsafe bitmap.deallocate() } - let bitmapValue = bitmap.pointee - let data = Data(bytesNoCopy: bitmapValue.pixels, count: Int(bitmapValue.pixelsCount), deallocator: .free) + let bitmapValue = unsafe bitmap.pointee + let data = unsafe Data(bytesNoCopy: bitmapValue.pixels, count: Int(bitmapValue.pixelsCount), deallocator: .free) + + let width = unsafe Int(bitmapValue.bitmapWidth) + let height = unsafe Int(bitmapValue.bitmapHeight) - let width = Int(bitmapValue.bitmapWidth) - let height = Int(bitmapValue.bitmapHeight) - assert(width > 0, "Invalid width of atlas") assert(height > 0, "Invalid width of atlas") self.saveAtlas(data, width: width, height: height, fileName: fileName) let texture = self.makeTextureAtlas(from: data, width: width, height: height) - return FontHandle(atlasTexture: texture, fontData: fontData) + return unsafe FontHandle(atlasTexture: texture, fontData: fontData) } } @@ -145,14 +149,14 @@ final class FontAtlasGenerator { stream.open() var header = AtlasHeader(width: width, height: height, dataSize: data.count) - withUnsafeBytes(of: &header) { ptr in - let bytes = ptr.baseAddress!.assumingMemoryBound(to: UInt8.self) - stream.write(bytes, maxLength: MemoryLayout.stride) + unsafe withUnsafeBytes(of: &header) { ptr in + let bytes = unsafe ptr.baseAddress!.assumingMemoryBound(to: UInt8.self) + unsafe stream.write(bytes, maxLength: MemoryLayout.stride) } - data.withUnsafeBytes { (bufferPtr: UnsafeRawBufferPointer) in - let bytes = bufferPtr.baseAddress!.assumingMemoryBound(to: UInt8.self) - stream.write(bytes, maxLength: data.count) + unsafe data.withUnsafeBytes { (bufferPtr: UnsafeRawBufferPointer) in + let bytes = unsafe bufferPtr.baseAddress!.assumingMemoryBound(to: UInt8.self) + unsafe stream.write(bytes, maxLength: data.count) } stream.close() @@ -178,17 +182,17 @@ final class FontAtlasGenerator { stream.open() let headerData: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: MemoryLayout.stride) - stream.read(headerData, maxLength: MemoryLayout.size) - let atlasHeader = UnsafeRawPointer(headerData).load(as: AtlasHeader.self) - + unsafe stream.read(headerData, maxLength: MemoryLayout.size) + let atlasHeader = unsafe UnsafeRawPointer(headerData).load(as: AtlasHeader.self) + let atlasData: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: atlasHeader.dataSize) - stream.read(atlasData, maxLength: atlasHeader.dataSize) - let data = Data(bytes: UnsafeRawPointer(atlasData), count: atlasHeader.dataSize) - + unsafe stream.read(atlasData, maxLength: atlasHeader.dataSize) + let data = unsafe Data(bytes: UnsafeRawPointer(atlasData), count: atlasHeader.dataSize) + defer { - headerData.deallocate() - atlasData.deallocate() - + unsafe headerData.deallocate() + unsafe atlasData.deallocate() + stream.close() } diff --git a/Sources/AdaText/Text/Font/FontHandle.swift b/Sources/AdaText/Text/Font/FontHandle.swift index 986cb6665..833dd6416 100644 --- a/Sources/AdaText/Text/Font/FontHandle.swift +++ b/Sources/AdaText/Text/Font/FontHandle.swift @@ -9,6 +9,7 @@ import AtlasFontGenerator import AdaRender /// Hold information about font data and atlas. +@safe final class FontHandle: Hashable, @unchecked Sendable { let atlasTexture: Texture2D @@ -20,33 +21,34 @@ final class FontHandle: Hashable, @unchecked Sendable { init( atlasTexture: Texture2D, - fontData: OpaquePointer) { + fontData: OpaquePointer + ) { self.atlasTexture = atlasTexture - self.fontData = fontData - - self.metrics = font_geometry_get_metrics(fontData) - self.fontName = String(cString: font_geometry_get_name(fontData)!) - self.geometryScale = font_geometry_get_scale(fontData) + unsafe self.fontData = fontData + + self.metrics = unsafe font_geometry_get_metrics(fontData) + self.fontName = unsafe String(cString: font_geometry_get_name(fontData)!) + self.geometryScale = unsafe font_geometry_get_scale(fontData) } deinit { - font_handle_destroy(self.fontData) + unsafe font_handle_destroy(self.fontData) } func getGlyph(for scalar: UInt32) -> Glyph? { - guard let glyph = font_handle_get_glyph_unicode(self.fontData, scalar) else { + guard let glyph = unsafe font_handle_get_glyph_unicode(self.fontData, scalar) else { return nil } - return Glyph(ref: glyph) + return unsafe Glyph(ref: glyph) } func getAdvance(_ advance: inout Double, _ currentUnicode: UInt32, _ nextUnicode: UInt32) { - font_handle_get_advance(self.fontData, &advance, currentUnicode, nextUnicode) + unsafe font_handle_get_advance(self.fontData, &advance, currentUnicode, nextUnicode) } var glyphsCount: Int { - return Int(font_handle_get_glyphs_count(self.fontData)) + unsafe Int(font_handle_get_glyphs_count(self.fontData)) } // MARK: Hashable @@ -76,37 +78,36 @@ final class FontHandle: Hashable, @unchecked Sendable { } extension FontHandle { - + @safe final class Glyph { let ref: OpaquePointer init(ref: OpaquePointer) { - self.ref = ref + unsafe self.ref = ref } deinit { - self.ref.deallocate() + unsafe self.ref.deallocate() } var advance: Double { - return font_glyph_get_advance(self.ref) + unsafe font_glyph_get_advance(self.ref) } func getQuadAtlasBounds(_ l: inout Double, _ b: inout Double, _ r: inout Double, _ t: inout Double) { - font_glyph_get_quad_atlas_bounds(self.ref, &l, &b, &r, &t) + unsafe font_glyph_get_quad_atlas_bounds(self.ref, &l, &b, &r, &t) } func getQuadPlaneBounds(_ pl: inout Double, _ pb: inout Double, _ pr: inout Double, _ pt: inout Double) { - font_glyph_get_quad_plane_bounds(self.ref, &pl, &pb, &pr, &pt) + unsafe font_glyph_get_quad_plane_bounds(self.ref, &pl, &pb, &pr, &pt) } } - } extension OpaquePointer { // TODO: Should we deallocate it in this place? func deallocate() { - UnsafeRawPointer(self).deallocate() + unsafe UnsafeRawPointer(self).deallocate() } } diff --git a/Sources/AdaText/Text/Font/FontResource.swift b/Sources/AdaText/Text/Font/FontResource.swift index dc83d2d56..3627acdb0 100644 --- a/Sources/AdaText/Text/Font/FontResource.swift +++ b/Sources/AdaText/Text/Font/FontResource.swift @@ -7,7 +7,12 @@ import AdaAssets import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif +import AtlasFontGenerator /// Contains font styles. public enum FontWeight: String { diff --git a/Sources/AdaText/Text/TextLayoutManager.swift b/Sources/AdaText/Text/TextLayoutManager.swift index 98e8a04a0..adde2c88a 100644 --- a/Sources/AdaText/Text/TextLayoutManager.swift +++ b/Sources/AdaText/Text/TextLayoutManager.swift @@ -8,6 +8,12 @@ import AdaRender import AdaUtils import Math +import AtlasFontGenerator +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif // FIXME: When text container updates each frame, than we have troubles with performance // FIXME: Fix TextRun, that should equals AttributedString.Run diff --git a/Sources/AdaText/Text2DEntity.swift b/Sources/AdaText/Text2DEntity.swift deleted file mode 100644 index 787edde6b..000000000 --- a/Sources/AdaText/Text2DEntity.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Text2DEntity.swift -// -// -// Created by v.prusakov on 5/2/24. -// - -import AdaECS -import AdaRender - -/// Create a new entity with Text2DComponent and without frustum culling. -final class Text2DEntity: Entity, @unchecked Sendable { - - public var textComponent: Text2DComponent { - get { - guard let component = self.components[Text2DComponent.self] else { - fatalError("Text2DEntity doesn't contains Text2DComponent") - } - - return component - } - - set { - self.components += newValue - } - } - - public override init(name: String = "Text2DEntity") { - super.init(name: name) - - self.components += Text2DComponent(text: AttributedText("")) - self.components += NoFrustumCulling() - } - - public init(name: String = "Text2DEntity", attributedText: AttributedText = AttributedText("")) { - super.init(name: name) - - self.components += Text2DComponent(text: attributedText) - self.components += NoFrustumCulling() - } -} diff --git a/Sources/AdaText/Text2DLayoutSystem.swift b/Sources/AdaText/Text2DLayoutSystem.swift deleted file mode 100644 index 0c415a1ea..000000000 --- a/Sources/AdaText/Text2DLayoutSystem.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// Text2DLayoutSystem.swift -// AdaEngine -// -// Created by v.prusakov on 3/7/23. -// - -import AdaECS -import AdaRender - -/// An object that store text layout manager -@Component -struct TextLayoutComponent { - let textLayout: TextLayoutManager -} - -/// System for layout text from ``Text2DComponent``. -@System() -public struct Text2DLayoutSystem { - - @Query, Visibility>(filter: [.stored, .added]) - private var textComponents - - public init(world: World) { } - - public func update(context: inout UpdateContext) { - self.textComponents.forEach { entity, text, visibility in - if visibility == .hidden { - return - } - - let textLayout = entity.components[TextLayoutComponent.self] ?? TextLayoutComponent(textLayout: TextLayoutManager()) - - let textContainer = TextContainer( - text: text.text, - textAlignment: text.textAlignment, - lineBreakMode: text.lineBreakMode, - lineSpacing: text.lineSpacing - ) - - textLayout.textLayout.setTextContainer(textContainer) - textLayout.textLayout.fitToSize(text.bounds.size) - - entity.components += textLayout - } - } -} diff --git a/Sources/AdaText/Text2DPlugin.swift b/Sources/AdaText/Text2DPlugin.swift deleted file mode 100644 index b4af7c26e..000000000 --- a/Sources/AdaText/Text2DPlugin.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Text2DPlugin.swift -// AdaEngine -// -// Created by v.prusakov on 3/5/23. -// - -import AdaApp -import AdaECS -import AdaRender - -/// Append text rendering systems to the scene. -public struct Text2DPlugin: Plugin { - - public init() {} - - public func setup(in app: AppWorlds) { - Text2DComponent.registerComponent() - - app -// .addSystem(ExctractTextSystem.self) - .addSystem(Text2DLayoutSystem.self) - } -} - -public struct Text2DRenderPlugin: Plugin { - - public init() {} - - public func setup(in app: AppWorlds) { -// world.addSystem(Text2DRenderSystem.self) - } -} diff --git a/Sources/AdaText/Text2DRenderSystem.swift b/Sources/AdaText/Text2DRenderSystem.swift deleted file mode 100644 index cf157a07c..000000000 --- a/Sources/AdaText/Text2DRenderSystem.swift +++ /dev/null @@ -1,204 +0,0 @@ -//// -//// Text2DRenderSystem.swift -//// AdaEngine -//// -//// Created by v.prusakov on 3/7/23. -//// -// -//import AdaAssets -//import AdaECS -//import AdaRender -//import AdaTransform -//import Math -// -//// FIXME: Should works with frustum culling -// -//// FIXME: WE SHOULD USE SAME SPRITE RENDERER!!!!!! -//public struct Text2DRenderSystem: RenderSystem, Sendable { -// -// public static let dependencies: [SystemDependency] = [ -//// .after(VisibilitySystem.self), -//// .before(BatchTransparent2DItemsSystem.self) -// ] -// -// static let quadPosition: [Vector4] = [ -// [-0.5, -0.5, 0.0, 1.0], -// [ 0.5, -0.5, 0.0, 1.0], -// [ 0.5, 0.5, 0.0, 1.0], -// [-0.5, 0.5, 0.0, 1.0] -// ] -// -// static let maxTexturesPerBatch = 16 -// -// static let textComponents = EntityQuery(where: .has(Text2DComponent.self) && .has(Transform.self) && .has(Visibility.self) && .has(TextLayoutComponent.self)) -// -// static let cameras = EntityQuery(where: -// .has(VisibleEntities.self) && -// .has(RenderItems.self) -// ) -// -// let textRenderPipeline: RenderPipeline -// -// public init(world: World) { -// let device = RenderEngine.shared.renderDevice -// -// let textShader = try! AssetsManager.loadSync( -// ShaderModule.self, -// at: "Assets/text.glsl", -// from: .module -// ) -// var piplineDesc = RenderPipelineDescriptor() -// piplineDesc.vertex = textShader.asset.getShader(for: .vertex) -// piplineDesc.fragment = textShader.asset.getShader(for: .fragment) -// piplineDesc.debugName = "Text Pipeline" -// -// piplineDesc.vertexDescriptor.attributes.append([ -// .attribute(.vector4, name: "position"), -// .attribute(.vector4, name: "foregroundColor"), -// .attribute(.vector4, name: "outlineColor"), -// .attribute(.vector2, name: "textureCoordinate"), -// .attribute(.int, name: "textureIndex") -// ]) -// -// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride -// piplineDesc.colorAttachments = [ColorAttachmentDescriptor(format: .bgra8, isBlendingEnabled: true)] -// -// let quadPipeline = device.createRenderPipeline(from: piplineDesc) -// self.textRenderPipeline = quadPipeline -// } -// -// public func update(context: inout UpdateContext) { -// context.world.performQuery(Self.cameras).forEach { entity in -// var (visibleEntities, renderItems) = entity.components[VisibleEntities.self, RenderItems.self] -// self.draw( -// world: context.world, -// visibleEntities: visibleEntities.entities, -// renderItems: &renderItems -// ) -// -// entity.components += renderItems -// } -// } -// -// // swiftlint:disable:next function_body_length -// private func draw( -// world: World, -// visibleEntities: [Entity], -// renderItems: inout RenderItems -// ) { -// let spriteDraw = SpriteDrawPass.identifier -// -// let texts = visibleEntities.filter { -// $0.components.has(Text2DComponent.self) && $0.components.has(TextLayoutComponent.self) -// } -// .sorted { lhs, rhs in -// lhs.components[Transform.self]!.position.z < rhs.components[Transform.self]!.position.z -// } -// -// for entity in texts { -// guard let textLayout = entity.components[TextLayoutComponent.self] else { -// continue -// } -// -// let currentBatchEntity = EmptyEntity() -// -// let transform = entity.components[Transform.self]! -// let worldTransform = entity.components[GlobalTransform.self]!.matrix -// -// let glyphs = textLayout.textLayout.getGlyphVertexData(transform: worldTransform) -// -// var spriteVerticies = glyphs.verticies -// -// if spriteVerticies.isEmpty { -// continue -// } -// -// // TODO: Redesign it latter -// var textures: [Texture2D] = [Texture2D].init(repeating: .whiteTexture, count: Self.maxTexturesPerBatch) -// glyphs.textures.compactMap { $0 }.enumerated().forEach { index, texture in -// textures[index] = texture -// } -// -// currentBatchEntity.components += BatchComponent(textures: textures) -// -// renderItems.items.append( -// Transparent2DRenderItem( -// entity: currentBatchEntity, -// batchEntity: currentBatchEntity, -// drawPassId: spriteDraw, -// renderPipeline: self.textRenderPipeline, -// sortKey: transform.position.z, -// batchRange: 0...stride, -// binding: 0 -// ) -// vertexBuffer.label = "Text2DRenderSystem_VertexBuffer" -// -// let indicies = Int(glyphs.indeciesCount * 4) -// -// var quadIndices = [UInt32].init(repeating: 0, count: indicies) -// -// var offset: UInt32 = 0 -// for index in stride(from: 0, to: indicies, by: 6) { -// quadIndices[index + 0] = offset + 0 -// quadIndices[index + 1] = offset + 1 -// quadIndices[index + 2] = offset + 2 -// -// quadIndices[index + 3] = offset + 2 -// quadIndices[index + 4] = offset + 3 -// quadIndices[index + 5] = offset + 0 -// -// offset += 4 -// } -// -// vertexBuffer.setData(&spriteVerticies, byteCount: spriteVerticies.count * MemoryLayout.stride) -// -// let quadIndexBuffer = device.createIndexBuffer( -// format: .uInt32, -// bytes: &quadIndices, -// length: indicies -// ) -// quadIndexBuffer.label = "Text2DRenderSystem_IndexBuffer" -// -// currentBatchEntity.components += SpriteDataComponent( -// vertexBuffer: vertexBuffer, -// indexBuffer: quadIndexBuffer -// ) -// } -// } -//} -// -//@System(dependencies: [ -// .after(Text2DLayoutSystem.self) -//]) -//struct ExctractTextSystem { -// -// @EntityQuery( -// where: .has(Text2DComponent.self) && .has(Transform.self) && -// .has(Visibility.self) && .has(TextLayoutComponent.self) -// ) -// private var textComponents -// -// init(world: World) { } -// -// func update(context: inout UpdateContext) { -// self.textComponents.forEach { entity in -// if entity.components[Visibility.self] == .hidden { -// return -// } -// -// let exctractedEntity = EmptyEntity() -// exctractedEntity.components += entity.components[Transform.self]! -// exctractedEntity.components += entity.components[Text2DComponent.self]! -// -// context.taskGroup.addTask { -// await Application.shared.renderWorld.addEntity(exctractedEntity) -// } -// } -// } -//} diff --git a/Sources/AdaText/TextBundle.swift b/Sources/AdaText/TextBundle.swift new file mode 100644 index 000000000..604c585b9 --- /dev/null +++ b/Sources/AdaText/TextBundle.swift @@ -0,0 +1,23 @@ +// +// TextBundle.swift +// +// +// Created by v.prusakov on 5/2/24. +// + +import AdaECS +import AdaRender + +@Bundle +public struct TextBundle { + public var textComponent = TextComponent(text: AttributedText("")) + let noFrustumCulling: NoFrustumCulling + + public init( + textComponent: TextComponent = TextComponent(text: AttributedText("")), + noFrustumCulling: NoFrustumCulling + ) { + self.textComponent = textComponent + self.noFrustumCulling = noFrustumCulling + } +} diff --git a/Sources/AdaText/Text2DComponent.swift b/Sources/AdaText/TextComponent.swift similarity index 90% rename from Sources/AdaText/Text2DComponent.swift rename to Sources/AdaText/TextComponent.swift index d857851fe..e0077241f 100644 --- a/Sources/AdaText/Text2DComponent.swift +++ b/Sources/AdaText/TextComponent.swift @@ -1,5 +1,5 @@ // -// Text2DComponent.swift +// TextComponent.swift // AdaEngine // // Created by v.prusakov on 3/5/23. @@ -8,9 +8,9 @@ import AdaECS import Math -/// Component for rendering 2D text on scene. +/// Component for rendering text on scene. @Component -public struct Text2DComponent { +public struct TextComponent { /// Text with style attributes. public var text: AttributedText diff --git a/Sources/AdaText/TextLayoutSystem.swift b/Sources/AdaText/TextLayoutSystem.swift new file mode 100644 index 000000000..c51b63a10 --- /dev/null +++ b/Sources/AdaText/TextLayoutSystem.swift @@ -0,0 +1,53 @@ +// +// TextLayoutSystem.swift +// AdaEngine +// +// Created by v.prusakov on 3/7/23. +// + +import AdaECS +import AdaRender +import AdaTransform +import Math + +/// An object that store text layout manager +@Component +public struct TextLayoutComponent { + public let textLayout: TextLayoutManager +} + +/// System for layout text from ``Text2DComponent``. +@PlainSystem +public struct TextLayoutSystem { + + @FilterQuery< + Ref, + Ref, + Visibility, + Or< + Changed, + Added, + Changed + > + > + private var textComponents + + public init(world: World) { } + + public func update(context: UpdateContext) { + self.textComponents.forEach { text, layout, visibility in + if visibility == .hidden { + return + } + + let textContainer = TextContainer( + text: text.text, + textAlignment: text.textAlignment, + lineBreakMode: text.lineBreakMode, + lineSpacing: text.lineSpacing + ) + layout.wrappedValue.textLayout.setTextContainer(textContainer) + layout.wrappedValue.textLayout.fitToSize(text.bounds.size) + } + } +} diff --git a/Sources/AdaText/TextPlugin.swift b/Sources/AdaText/TextPlugin.swift new file mode 100644 index 000000000..4af156cca --- /dev/null +++ b/Sources/AdaText/TextPlugin.swift @@ -0,0 +1,67 @@ +// +// Text2DPlugin.swift +// AdaEngine +// +// Created by v.prusakov on 3/5/23. +// + +import AdaAssets +import AdaApp +import AdaECS +import AdaRender + +/// Append text rendering systems to the scene. +public struct TextPlugin: Plugin { + + public init() {} + + public func setup(in app: AppWorlds) { + TextComponent.registerComponent() + + app + .main + .registerRequiredComponent(TextLayoutComponent.self, for: TextComponent.self) { + TextLayoutComponent(textLayout: TextLayoutManager()) + } + .insertResource(RenderPipelines(configurator: TextPipeline())) + .addSystem(TextLayoutSystem.self) + } +} + +public struct TextPipeline: RenderPipelineConfigurator { + private let shader: AssetHandle + + public init() { + self.shader = try! AssetsManager.loadSync( + ShaderModule.self, + at: "Assets/text.glsl", + from: .module + ) + } + + public func configurate( + with configuration: RenderPipelineEmptyConfiguration + ) -> RenderPipelineDescriptor { + var piplineDesc = RenderPipelineDescriptor() + piplineDesc.vertex = shader.asset.getShader(for: .vertex) + piplineDesc.fragment = shader.asset.getShader(for: .fragment) + piplineDesc.debugName = "Text Pipeline" + + piplineDesc.vertexDescriptor.attributes.append([ + .attribute(.vector4, name: "position"), + .attribute(.vector4, name: "foregroundColor"), + .attribute(.vector4, name: "outlineColor"), + .attribute(.vector2, name: "textureCoordinate"), + .attribute(.int, name: "textureIndex") + ]) + + piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride + piplineDesc.colorAttachments = [ + RenderPipelineColorAttachmentDescriptor( + format: .bgra8, + isBlendingEnabled: true + ) + ] + return piplineDesc + } +} diff --git a/Sources/AdaTilemap/LdtkTileMap.swift b/Sources/AdaTilemap/LdtkTileMap.swift index 4cf8e45da..7937d2ba2 100644 --- a/Sources/AdaTilemap/LdtkTileMap.swift +++ b/Sources/AdaTilemap/LdtkTileMap.swift @@ -10,9 +10,14 @@ import AdaAssets import AdaUtils import AdaSprite import AdaRender +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Logging import Math +import OrderedCollections /// Namespace for LDtk public enum LDtk { } @@ -49,7 +54,7 @@ extension LDtk { /// - Note: Use ``TileMap/resourcePath`` field to get runtime path to your LDtk file. public private(set) var isHotReloadingEnabled: Bool = false - public required init(from decoder: AssetDecoder) throws { + public required init(from decoder: AssetDecoder) async throws { let pathExt = decoder.assetMeta.filePath.pathExtension guard pathExt == "ldtk" || pathExt == "json" else { @@ -64,13 +69,11 @@ extension LDtk { self?.onLDtkFileMapChanged(paths: paths) } - Task { - try await self.loadLdtkProject(from: decoder.assetData) - } + try await self.loadLdtkProject(from: decoder.assetData) } - public override func encodeContents(with encoder: AssetEncoder) throws { - try super.encodeContents(with: encoder) + public override func encodeContents(with encoder: AssetEncoder) async throws { + try await super.encodeContents(with: encoder) } /// Load level from LDtk project at index. @@ -79,6 +82,7 @@ extension LDtk { // swiftlint:disable:next cyclomatic_complexity function_body_length public func loadLevel(at index: Int) { guard let project else { + assertionFailure("LDtk Project is empty at path \(filePath)") return } @@ -92,11 +96,13 @@ extension LDtk { for layerInstance in level.layerInstances where layerInstance.visible { guard let layer = self.layers.first(where: { $0.id == layerInstance.layerDefUid }) else { - fatalError("Could not find a layer for id \(layerInstance.layerDefUid)") + assertionFailure("Could not find a layer for id \(layerInstance.layerDefUid)") + return } guard let projectLayer = project.defs.layers.first(where: { $0.uid == layerInstance.layerDefUid }) else { - fatalError("Could not find a layer in project for id: \(layerInstance.layerDefUid)") + assertionFailure("Could not find a layer in project for id: \(layerInstance.layerDefUid)") + return } switch layerInstance.__type { @@ -293,7 +299,10 @@ extension LDtk { /// - Parameters: /// - atlasCoordinates: The atlas coordinates to create the tile for. /// - entityInstance: The entity instance to create the tile for. - public func createTile(at atlasCoordinates: PointInt, entityInstance: LDtk.EntityInstance) { + public func createTile( + at atlasCoordinates: PointInt, + entityInstance: LDtk.EntityInstance + ) { let entity = AdaECS.Entity(name: entityInstance.identifier) if let source = self.tileSet?.sources[entityInstance.tile.tilesetUid] as? TextureAtlasTileSource { diff --git a/Sources/AdaTilemap/TileMap.swift b/Sources/AdaTilemap/TileMap.swift index 14d9f8913..d2d78b862 100644 --- a/Sources/AdaTilemap/TileMap.swift +++ b/Sources/AdaTilemap/TileMap.swift @@ -8,9 +8,10 @@ import AdaAssets import Math @_spi(Runtime) import AdaUtils +import OrderedCollections /// A tile map. -public class TileMap: Asset, @unchecked Sendable { +public class TileMap: @unsafe Asset, @unchecked Sendable { /// The tile set of the tile map. public var tileSet: TileSet = TileSet() { @@ -37,7 +38,7 @@ public class TileMap: Asset, @unchecked Sendable { /// /// - Parameter decoder: The decoder to initialize the tile map from. /// - Throws: An error if the tile map cannot be initialized from the decoder. - public required init(from decoder: AssetDecoder) throws { + public required init(from decoder: AssetDecoder) async throws { let fileContent = try decoder.decode(FileContent.self) self.tileSet = fileContent.tileSet @@ -59,7 +60,7 @@ public class TileMap: Asset, @unchecked Sendable { /// /// - Parameter encoder: The encoder to encode the tile map to. /// - Throws: An error if the tile map cannot be encoded to the encoder. - public func encodeContents(with encoder: AssetEncoder) throws { + public func encodeContents(with encoder: AssetEncoder) async throws { var layers = [FileContent.Layer]() for layer in self.layers { diff --git a/Sources/AdaTilemap/TileMapComponent.swift b/Sources/AdaTilemap/TileMapComponent.swift index 2aab4c6db..7e432c091 100644 --- a/Sources/AdaTilemap/TileMapComponent.swift +++ b/Sources/AdaTilemap/TileMapComponent.swift @@ -17,7 +17,7 @@ public struct TileMapComponent { /// Contains information about entities /// /// Each tile layer contains root entity that holds tile sprite entitis with physic bodies. - internal var tileLayers: [TileMapLayer.ID: Entity] = [:] + internal var tileLayers: [TileMapLayer.ID: Entity.ID] = [:] public init(tileMap: TileMap) { self.tileMap = tileMap diff --git a/Sources/AdaTilemap/TileMapPlugin.swift b/Sources/AdaTilemap/TileMapPlugin.swift index bdc0e2e02..892f655af 100644 --- a/Sources/AdaTilemap/TileMapPlugin.swift +++ b/Sources/AdaTilemap/TileMapPlugin.swift @@ -6,6 +6,7 @@ // import AdaApp +import AdaECS public struct TileMapPlugin: Plugin { diff --git a/Sources/AdaTilemap/TileMapSystem.swift b/Sources/AdaTilemap/TileMapSystem.swift index fd1031a3b..ff58f15d9 100644 --- a/Sources/AdaTilemap/TileMapSystem.swift +++ b/Sources/AdaTilemap/TileMapSystem.swift @@ -12,9 +12,10 @@ import AdaPhysics import AdaSprite import Logging import Math +import OrderedCollections // FIXME: a lot of sprites drop fps. -@System +@PlainSystem public struct TileMapSystem: Sendable { let logger = Logger(label: "tilemap") @@ -22,12 +23,18 @@ public struct TileMapSystem: Sendable { @Query, Transform> private var tileMap + @Res + private var physicsWorld + + @Commands + private var commands + public init(world: World) { } - public func update(context: inout UpdateContext) { - let physicsWorld = context.world.getResource(Physics2DWorldComponent.self)?.world + public func update(context: UpdateContext) { + let physicsWorld = physicsWorld?.world - for (entity, tileMapComponent, transform) in tileMap { + tileMap.forEach { (entity, tileMapComponent, transform) in let tileMap = tileMapComponent.tileMap if !tileMap.needsUpdate { @@ -35,13 +42,16 @@ public struct TileMapSystem: Sendable { } for layer in tileMap.layers { - if let ent = tileMapComponent.tileLayers[layer.id] { - self.setEntityActive(ent, isActive: layer.isEnabled) + if + let entityId = tileMapComponent.tileLayers[layer.id], + let entity = context.world.getEntityByID(entityId) + { + self.setEntityActive(entity, isActive: layer.isEnabled) } self.addTiles( for: layer, - tileMapComponent: &tileMapComponent.wrappedValue, + tileMapComponent: tileMapComponent, transform: transform, entity: entity, physicsWorld: physicsWorld, @@ -62,7 +72,7 @@ public struct TileMapSystem: Sendable { private func addTiles( for layer: TileMapLayer, - tileMapComponent: inout TileMapComponent, + tileMapComponent: Ref, transform: Transform, entity: Entity, physicsWorld: PhysicsWorld2D?, @@ -72,9 +82,13 @@ public struct TileMapSystem: Sendable { let scale = Vector3(1) if layer.needUpdates { - tileMapComponent.tileLayers[layer.id]?.removeFromScene(recursively: true) + if let entity = tileMapComponent.tileLayers[layer.id] { + commands.entity(entity).removeFromWorld(recursively: true) + } - let tileParent = Entity() + let tileParent = commands.spawn() { + RelationshipComponent() + } for (position, tile) in layer.tileCells { guard let source = tileSet.sources[tile.sourceId] else { @@ -86,14 +100,17 @@ public struct TileMapSystem: Sendable { let position = Vector3(x: Float(position.x), y: Float(position.y), z: Float(layer.zIndex)) let tileEntity: Entity - switch source { case let atlasSource as TextureAtlasTileSource: let texture = atlasSource.getTexture(at: tile.atlasCoordinates) - tileEntity = Entity() - tileEntity.components += SpriteComponent(texture: AssetHandle(texture), tintColor: tileData.modulateColor) - tileEntity.components += Transform(scale: scale, position: position) + tileEntity = Entity { + SpriteComponent( + texture: AssetHandle(texture), + tintColor: tileData.modulateColor + ) + Transform(scale: scale, position: position) + } case let entitySource as TileEntityAtlasSource: tileEntity = entitySource.getEntity(at: tile.atlasCoordinates) tileEntity.components += Transform(scale: scale, position: position) @@ -113,10 +130,8 @@ public struct TileMapSystem: Sendable { // } tileParent.addChild(tileEntity) - world.addEntity(tileEntity) } - world.addEntity(tileParent) - tileMapComponent.tileLayers[layer.id] = tileParent + tileMapComponent.tileLayers[layer.id] = tileParent.entityId layer.updateDidFinish() } } diff --git a/Sources/AdaTilemap/TileSet.swift b/Sources/AdaTilemap/TileSet.swift index 3f5467147..3b7448a95 100644 --- a/Sources/AdaTilemap/TileSet.swift +++ b/Sources/AdaTilemap/TileSet.swift @@ -10,7 +10,7 @@ import AdaPhysics import OrderedCollections import Math -public class TileSet: Asset, Codable, @unchecked Sendable { +public class TileSet: @unsafe Asset, Codable, @unchecked Sendable { struct PhysicsLayer { var collisionLayer: CollisionGroup = .default @@ -103,7 +103,7 @@ extension TileSet { let sourceContainer = try sourcesContainer.nestedContainer(keyedBy: SourceCodingKeys.self) let sourceType = try sourceContainer.decode(String.self, forKey: .type) - guard let value = TileSource.types[sourceType] else { + guard let value = unsafe TileSource.types[sourceType] else { continue } diff --git a/Sources/AdaTilemap/TileSource.swift b/Sources/AdaTilemap/TileSource.swift index fc25f3a52..d6506db11 100644 --- a/Sources/AdaTilemap/TileSource.swift +++ b/Sources/AdaTilemap/TileSource.swift @@ -69,7 +69,7 @@ public class TileSource: Codable, @unchecked Sendable { /// Call this function if you inherited from TileSource. public static func registerTileSource() { - self.types[String(reflecting: self)] = Self.self + unsafe self.types[String(reflecting: self)] = Self.self } } diff --git a/Sources/AdaTransform/TransformSystem.swift b/Sources/AdaTransform/TransformSystem.swift index 22457ae0c..40fbd6ac2 100644 --- a/Sources/AdaTransform/TransformSystem.swift +++ b/Sources/AdaTransform/TransformSystem.swift @@ -6,48 +6,51 @@ // import AdaECS +import AdaUtils import Math /// A system that updates the global transform of the entity. -@System +@PlainSystem public struct TransformSystem { - @EntityQuery(where: .has(Transform.self)) + @FilterQuery, Without>> private var query - + + @Commands + private var commands + public init(world: World) { } - public func update(context: inout UpdateContext) { - self.query.forEach { entity in - if entity.components.isComponentChanged(Transform.self) - || !entity.components.has(GlobalTransform.self) - { - let transform = entity.components[Transform.self]! - let globalTransform = GlobalTransform(matrix: transform.matrix) - entity.components += globalTransform - } + public func update(context: UpdateContext) async { + await self.query.parallel().forEach { entity, transform in + let globalTransform = GlobalTransform(matrix: transform.matrix) + commands.entity(entity.id) + .insert(globalTransform) } } } /// A system that updates the global transform of the children of the entity. -@System(dependencies: [ +@PlainSystem(dependencies: [ .after(TransformSystem.self) ]) public struct ChildTransformSystem { - @EntityQuery(where: .has(Transform.self)) + @FilterQuery> private var query - + + @Commands + private var commands + public init(world: World) { } - public func update(context: inout UpdateContext) { - self.query.forEach { entity in - guard entity.components.isComponentChanged(Transform.self) && !entity.children.isEmpty else { + public func update(context: UpdateContext) async { + await self.query.parallel().forEach { entity, globalTransform in + guard !entity.children.isEmpty else { return } - updateChildren(entity.children, parentTransform: entity.components[GlobalTransform.self]!) + updateChildren(entity.children, parentTransform: globalTransform) } } @@ -57,13 +60,13 @@ public struct ChildTransformSystem { /// - Parameter parentTransform: The parent transform of the entity. private func updateChildren(_ children: [Entity], parentTransform: GlobalTransform) { for child in children { - guard let childTransform = child.components[Transform.self] else { + guard child.components.has(Transform.self) else { continue } - + + let childTransform = child.components.get(Transform.self) let newMatrix = parentTransform.matrix * childTransform.matrix -// child.components += Transform(matrix: newMatrix) - child.components += GlobalTransform(matrix: newMatrix) + commands.entity(child.id).insert(GlobalTransform(matrix: newMatrix)) if !child.children.isEmpty { updateChildren(child.children, parentTransform: GlobalTransform(matrix: newMatrix)) diff --git a/Sources/AdaUI/DSL/Gestures/Gesture.swift b/Sources/AdaUI/DSL/Gestures/Gesture.swift index 774524ed4..5ff6a34e8 100644 --- a/Sources/AdaUI/DSL/Gestures/Gesture.swift +++ b/Sources/AdaUI/DSL/Gestures/Gesture.swift @@ -81,7 +81,7 @@ public class _Gesture { } extension Never: Gesture { - public typealias Value = Void + public typealias Value = Never } public extension Gesture where Body == Never { diff --git a/Sources/AdaUI/DSL/Graph.swift b/Sources/AdaUI/DSL/Graph.swift index c1d8d7073..7abe4dbc5 100644 --- a/Sources/AdaUI/DSL/Graph.swift +++ b/Sources/AdaUI/DSL/Graph.swift @@ -6,7 +6,11 @@ // import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif @MainActor final class ViewGraph { @@ -102,9 +106,9 @@ public struct _ViewGraphNode: Equatable { // if its pod, we can compare it together using memcmp. if _isPOD(Value.self) { let memSize = MemoryLayout.size - return withUnsafePointer(to: lhs.value) { lhsPtr in - withUnsafePointer(to: rhs.value) { rhsPtr in - memcmp(lhsPtr, rhsPtr, memSize) == 0 + return unsafe withUnsafePointer(to: lhs.value) { lhsPtr in + unsafe withUnsafePointer(to: rhs.value) { rhsPtr in + unsafe memcmp(lhsPtr, rhsPtr, memSize) == 0 } } } else { diff --git a/Sources/AdaUI/DSL/ImageView.swift b/Sources/AdaUI/DSL/ImageView.swift index 5056e3845..71522d203 100644 --- a/Sources/AdaUI/DSL/ImageView.swift +++ b/Sources/AdaUI/DSL/ImageView.swift @@ -6,78 +6,52 @@ // import AdaAssets -import AdaRender +@_spi(Internal) import AdaRender import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Math -/// A view that displays an image. -public struct ImageView: View, ViewNodeBuilder { - - /// The storage type. - public enum _Storage { - /// The image. - case image(Image) - /// The texture. - case texture(Texture2D) - } +/// The render mode of the image view. +public enum ImageRenderMode: Codable, Sendable { + /// The original render mode. + case original + /// The template render mode. + case template +} +extension Image: View, ViewNodeBuilder { public typealias Body = Never public var body: Never { fatalError() } - let storage: _Storage - var isResizable: Bool = false - var renderMode: ImageRenderMode = .original - - /// Initialize a new image view. - /// - /// - Parameter image: The image. - public init(_ image: Image) { - self.storage = .image(image) - } - - /// Initialize a new image view. - /// - /// - Parameter path: The path to the image. - /// - Parameter bundle: The bundle. - public init(_ path: String, bundle: Bundle) { - self.storage = .texture(try! AssetsManager.loadSync( - Texture2D.self, - at: path, - from: bundle - ).asset) - } - - /// Initialize a new image view. - /// - /// - Parameter texture: The texture. - public init(_ texture: Texture2D) { - self.storage = .texture(texture) - } - - /// Build a view node. - /// - /// - Parameter context: The build context. - /// - Returns: The view node. func buildViewNode(in context: BuildContext) -> ViewNode { ImageViewNode( - storage: self.storage, - isResizable: self.isResizable, - renderMode: self.renderMode, + image: self, + isResizable: self.options[Keys.resizable.rawValue] as! Bool, + renderMode: self.options[Keys.renderMode.rawValue] as! ImageRenderMode, tintColor: context.environment.foregroundColor, content: self ) } } -public extension ImageView { - /// Make the image view resizable. +public extension Image { + + private enum Keys: String { + case resizable + case renderMode + } + + /// Make the image resizable. /// /// - Returns: The image view. - func resizable() -> ImageView { + func resizable() -> Image { var newValue = self - newValue.isResizable = true + newValue.options[Keys.resizable.rawValue] = true return newValue } @@ -85,13 +59,14 @@ public extension ImageView { /// /// - Parameter mode: The render mode. /// - Returns: The image view. - func renderMode(_ mode: ImageRenderMode) -> ImageView { + func renderMode(_ mode: ImageRenderMode) -> Image { var newValue = self - newValue.renderMode = mode + newValue.options[Keys.renderMode.rawValue] = mode return newValue } } + final class ImageViewNode: ViewNode { /// The texture. @@ -104,19 +79,13 @@ final class ImageViewNode: ViewNode { let tintColor: Color? init( - storage: ImageView._Storage, + image: Image, isResizable: Bool, renderMode: ImageRenderMode, tintColor: Color?, content: Content ) { - switch storage { - case .image(let image): - self.texture = Texture2D(image: image) - case .texture(let texture2D): - self.texture = texture2D - } - + self.texture = Texture2D(image: image) self.tintColor = tintColor self.renderMode = renderMode self.isResizable = isResizable diff --git a/Sources/AdaUI/DSL/Modifiers/DrawingGroupModifier.swift b/Sources/AdaUI/DSL/Modifiers/DrawingGroupModifier.swift index 1e035e64c..f1faad767 100644 --- a/Sources/AdaUI/DSL/Modifiers/DrawingGroupModifier.swift +++ b/Sources/AdaUI/DSL/Modifiers/DrawingGroupModifier.swift @@ -5,6 +5,8 @@ // Created by vladislav.prusakov on 31.07.2024. // +import Math + public extension View { func drawingGroup() -> some View { self.modifier(DrawingGroupModifier(content: self)) diff --git a/Sources/AdaUI/DSL/Modifiers/EventManagerModifier.swift b/Sources/AdaUI/DSL/Modifiers/EventManagerModifier.swift index 722b9d80a..5f2af5dd7 100644 --- a/Sources/AdaUI/DSL/Modifiers/EventManagerModifier.swift +++ b/Sources/AdaUI/DSL/Modifiers/EventManagerModifier.swift @@ -53,7 +53,7 @@ private final class EventManagerNode: ViewModifierNode { } struct EventManagerEnvironmentKey: EnvironmentKey { - nonisolated(unsafe) static var defaultValue = EventManager.default + static let defaultValue = EventManager.default } public extension EnvironmentValues { diff --git a/Sources/AdaUI/DSL/Modifiers/MouseModifier.swift b/Sources/AdaUI/DSL/Modifiers/MouseModifier.swift index b29198a61..7a8fe5ae6 100644 --- a/Sources/AdaUI/DSL/Modifiers/MouseModifier.swift +++ b/Sources/AdaUI/DSL/Modifiers/MouseModifier.swift @@ -8,13 +8,13 @@ import AdaInput public extension View { - func cursorShape(_ shape: Input.CursorShape) -> some View { - CursorShapeModifier(shape: shape, content: self) - } +// func cursorShape(_ shape: Input.CursorShape) -> some View { +// CursorShapeModifier(shape: shape, content: self) +// } - func onHover(perform action: (Bool) -> Void) -> some View { - EmptyView() - } +// func onHover(perform action: (Bool) -> Void) -> some View { +// EmptyView() +// } } struct CursorShapeModifier: View, ViewNodeBuilder { @@ -43,9 +43,9 @@ class CursorShapeModifierNode: ViewModifierNode { override func onMouseEvent(_ event: MouseEvent) { // Just moved if event.button == .none && event.phase == .changed { - Input.pushCursorShape(shape) +// Input.pushCursorShape(shape) } - Input.popCursorShape() +// Input.popCursorShape() } } diff --git a/Sources/AdaUI/DSL/Modifiers/OpacityModifier.swift b/Sources/AdaUI/DSL/Modifiers/OpacityModifier.swift index 8789e1a5e..ac15ea9f9 100644 --- a/Sources/AdaUI/DSL/Modifiers/OpacityModifier.swift +++ b/Sources/AdaUI/DSL/Modifiers/OpacityModifier.swift @@ -5,6 +5,8 @@ // Created by vladislav.prusakov on 31.07.2024. // +import Math + public extension View { func opacity(_ opacity: Float) -> some View { modifier(_OpacityView(opacity: opacity, content: self)) diff --git a/Sources/AdaUI/DSL/Modifiers/View+Observable.swift b/Sources/AdaUI/DSL/Modifiers/View+Observable.swift index a85513567..85e44bd72 100644 --- a/Sources/AdaUI/DSL/Modifiers/View+Observable.swift +++ b/Sources/AdaUI/DSL/Modifiers/View+Observable.swift @@ -6,7 +6,7 @@ // import AdaUtils -@preconcurrency import Observation +@unsafe @preconcurrency import Observation public extension View { diff --git a/Sources/AdaUI/DSL/Modifiers/VisibilityViewModifier.swift b/Sources/AdaUI/DSL/Modifiers/VisibilityViewModifier.swift index 91a1a9640..cf3d44e8c 100644 --- a/Sources/AdaUI/DSL/Modifiers/VisibilityViewModifier.swift +++ b/Sources/AdaUI/DSL/Modifiers/VisibilityViewModifier.swift @@ -5,6 +5,8 @@ // Created by Vladislav Prusakov on 24.06.2024. // +import Math + public extension View { /// Adds an action to perform before this view appears. /// - Parameter action: The action to perform. If action is nil, the call has no effect. diff --git a/Sources/AdaUI/DSL/Nodes/ViewTree.swift b/Sources/AdaUI/DSL/Nodes/ViewTree.swift index e2f968903..c0a39745c 100644 --- a/Sources/AdaUI/DSL/Nodes/ViewTree.swift +++ b/Sources/AdaUI/DSL/Nodes/ViewTree.swift @@ -7,7 +7,11 @@ import AdaInput import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Math @MainActor diff --git a/Sources/AdaUI/DSL/PropertyWrappers/State.swift b/Sources/AdaUI/DSL/PropertyWrappers/State.swift index 034e73994..b2e45c79d 100644 --- a/Sources/AdaUI/DSL/PropertyWrappers/State.swift +++ b/Sources/AdaUI/DSL/PropertyWrappers/State.swift @@ -18,11 +18,11 @@ public struct State: UpdatableProperty, PropertyStoragable { let _storage: StateStorage public var wrappedValue: Value { - get { - return _storage.value + _read { + yield _storage.value } - nonmutating set { - _storage.value = newValue + nonmutating _modify { + yield &_storage.value _storage.update() } } @@ -31,7 +31,8 @@ public struct State: UpdatableProperty, PropertyStoragable { Binding { self.wrappedValue } set: { newValue in - self.wrappedValue = newValue + self._storage.value = newValue + self._storage.update() } } diff --git a/Sources/AdaUI/DSL/Text.swift b/Sources/AdaUI/DSL/Text.swift index 7a313383f..4b0690a57 100644 --- a/Sources/AdaUI/DSL/Text.swift +++ b/Sources/AdaUI/DSL/Text.swift @@ -24,7 +24,6 @@ public struct Text { init(_ storage: Storage) { self.storage = storage } - } extension Text: View, ViewNodeBuilder { diff --git a/Sources/AdaUI/GUIRenderContext.swift b/Sources/AdaUI/GUIRenderContext.swift index f018097ae..1f124ed2f 100644 --- a/Sources/AdaUI/GUIRenderContext.swift +++ b/Sources/AdaUI/GUIRenderContext.swift @@ -22,7 +22,8 @@ public struct UIGraphicsContext { /// Returns current transform. public private(set) var transform: Transform3D = .identity -// private(set) var currentDrawContext: Renderer2D.DrawContext? + private var renderEncoder: RenderCommandEncoder! + private unowned let renderDevice: RenderDevice private var clipPath: Path? public var opacity: Float = 1 @@ -31,23 +32,22 @@ public struct UIGraphicsContext { public var environment: EnvironmentValues = EnvironmentValues() // Used for internal and debug values. - @_spi(AdaEngineEditor) public var _environment: EnvironmentValues = EnvironmentValues() + package var _environment: EnvironmentValues = EnvironmentValues() private var viewMatrix: Transform3D = .identity @MainActor - public init(window: UIWindow) { - var camera = Camera(window: .windowId(window.id)) - camera.isActive = true - camera.projection = .orthographic + public init(camera: Camera, renderDevice: RenderDevice) { self.camera = camera + self.renderDevice = renderDevice } - public init(texture: RenderTexture) { + public init(texture: RenderTexture, renderDevice: RenderDevice) { var camera = Camera(renderTarget: texture) camera.isActive = true camera.projection = .orthographic self.camera = camera + self.renderDevice = renderDevice } public mutating func beginDraw(in size: Size, scaleFactor: Float) { diff --git a/Sources/AdaUI/DSL/Path.swift b/Sources/AdaUI/Path.swift similarity index 100% rename from Sources/AdaUI/DSL/Path.swift rename to Sources/AdaUI/Path.swift diff --git a/Sources/AdaUI/UI/UISystem.swift b/Sources/AdaUI/Plugins/UIComponentSystem.swift similarity index 51% rename from Sources/AdaUI/UI/UISystem.swift rename to Sources/AdaUI/Plugins/UIComponentSystem.swift index 820135fd8..9635ef5cd 100644 --- a/Sources/AdaUI/UI/UISystem.swift +++ b/Sources/AdaUI/Plugins/UIComponentSystem.swift @@ -8,41 +8,60 @@ import AdaECS import AdaInput import AdaTransform +import AdaRender import AdaUtils import Math -@System +@PlainSystem public struct UIComponentSystem: Sendable { - @EntityQuery(where: .has(UIComponent.self) && .has(GlobalTransform.self)) + @Query private var uiComponents + @ResMut + private var input: Input? + + @Res + private var deltaTime + + @Res + private var windowManager + + @ResMut + private var pendingViews + public init(world: World) {} - public func update(context: inout UpdateContext) { -// guard let scene = context.scene else { -// return -// } -// -// for entity in self.uiComponents { -// context.taskGroup.addTask { -// await update(entity: entity, scene: scene, deltaTime: context.deltaTime) -// } -// } + @MainActor + public func update(context: UpdateContext) { + self.uiComponents.forEach { entity, component, transform in +// update( +// entity: entity, +// component: component, +// globalTransform: transform, +// window: UIWindow, +// deltaTime: deltaTime.deltaTime +// ) + } } } private extension UIComponentSystem { @MainActor - func update(entity: Entity, window: UIWindow, deltaTime: TimeInterval) async { - let (component, globalTransform) = entity.components[UIComponent.self, GlobalTransform.self] + @inline(__always) + func update( + entity: Entity, + component: UIComponent, + globalTransform: GlobalTransform, + window: UIWindow, + deltaTime: TimeInterval + ) async { let view = component.view let behaviour = component.behaviour view.window = window if let viewOwner = (view as? ViewOwner) { var environment = EnvironmentValues() -// environment.scene = WeakBox(value: scene) environment.entity = WeakBox(value: entity) viewOwner.updateEnvironment(environment) } @@ -55,21 +74,20 @@ private extension UIComponentSystem { view.layoutSubviews() } - var renderContext = UIGraphicsContext(window: window) - renderContext.beginDraw(in: window.frame.size, scaleFactor: 1) - view.draw(with: renderContext) - renderContext.commitDraw() + pendingViews.views.append(view) case .default: view.transform3D = globalTransform.matrix } - for event in Input.getInputEvents() { - guard view.canRespondToAction(event) else { - continue - } + if let input = self.input { + for event in input.getInputEvents() { + guard view.canRespondToAction(event) else { + continue + } - let responder = view.findFirstResponder(for: event) ?? view - responder.onEvent(event) + let responder = view.findFirstResponder(for: event) ?? view + responder.onEvent(event) + } } await view.update(deltaTime) @@ -83,4 +101,6 @@ public extension EnvironmentValues { /// The game scene where view attached. @Entry internal(set) var entity: WeakBox? + + @Entry internal(set) var input: Ref? } diff --git a/Sources/AdaUI/Plugins/UIPlugin.swift b/Sources/AdaUI/Plugins/UIPlugin.swift new file mode 100644 index 000000000..66665c670 --- /dev/null +++ b/Sources/AdaUI/Plugins/UIPlugin.swift @@ -0,0 +1,103 @@ +// +// UIPlugin.swift +// AdaEngine +// +// Created by vladislav.prusakov on 19.08.2024. +// + +import AdaApp +import AdaECS +@_spi(Internal) import AdaInput +import AdaRender +import AdaUtils +import Math + +public struct UIPlugin: Plugin { + public init() { } + + public func setup(in app: AppWorlds) { + UIComponent.registerComponent() + + app + .addSystem(UpdateWindowManagerSystem.self, on: .preUpdate) + .addSystem(UIComponentSystem.self) + .insertResource(UIDrawPendingViews(views: [])) + } +} + +public struct WindowPlugin: Plugin { + let primaryWindow: UIWindow? + + public init(primaryWindow: UIWindow? = nil) { + self.primaryWindow = primaryWindow + } + + public func setup(in app: AppWorlds) { + guard let windowSettings = app.getResource(WindowSettings.self) else { + return + } + + if let primaryWindow { + primaryWindow.showWindow(makeFocused: true) + app + .insertResource(PrimaryWindow(window: primaryWindow)) + .insertResource(PrimaryWindowId(windowId: primaryWindow.id)) + } else { + let window = UIWindow() + window.title = windowSettings.title ?? "App" + window.minSize = windowSettings.minimumSize + window.frame = Rect(origin: .zero, size: windowSettings.minimumSize) + window.setWindowMode( + windowSettings.windowMode == .fullscreen ? .fullscreen : .windowed + ) + window.showWindow(makeFocused: true) + app + .insertResource(PrimaryWindow(window: window)) + .insertResource(PrimaryWindowId(windowId: window.id)) + } + } +} + +public struct PrimaryWindow: Resource { + public let window: UIWindow +} + +public struct WindowManagerResource: Resource { + public let windowManager: UIWindowManager + + public init(windowManager: UIWindowManager) { + self.windowManager = windowManager + } +} + +@System +@inline(__always) +@MainActor +func UpdateWindowManager( + _ context: WorldUpdateContext, + _ windowManager: Res, + _ pendingViews: ResMut, + _ input: Res, + _ deltaTime: Res +) async { + let windowManager = windowManager.windowManager + let deltaTime = deltaTime.deltaTime + let windows = windowManager.windows + for window in windows { + let menuBuilder = windowManager.menuBuilder(for: window) + menuBuilder?.updateIfNeeded() + + for event in input.eventsPool where event.window == window.id { + window.sendEvent(event) + } + + await window.internalUpdate(deltaTime) + if window.canDraw { + pendingViews.views.append(window) + } + } +} + +struct UIDrawPendingViews: Resource { + var views: [UIView] +} diff --git a/Sources/AdaUI/Plugins/UIRenderPlugin.swift b/Sources/AdaUI/Plugins/UIRenderPlugin.swift new file mode 100644 index 000000000..77fab7329 --- /dev/null +++ b/Sources/AdaUI/Plugins/UIRenderPlugin.swift @@ -0,0 +1,72 @@ +// +// UIRenderPlugin.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 04.12.2025. +// + +import AdaApp +import AdaECS +import AdaRender + +public struct UIRenderPlugin: Plugin { + public func setup(in app: borrowing AdaApp.AppWorlds) { + app.main.registerRequiredComponent( + RenderItems.self, + for: Camera.self + ) { + RenderItems() + } + guard let renderWorld = app.getSubworldBuilder(by: .renderWorld) else { + return + } + let renderGraph = renderWorld.getRefResource(RenderGraph.self) + renderGraph.wrappedValue.addNode(UIRenderNode()) + renderWorld.addSystem(UIRenderPreparingSystem.self, on: .extract) + } +} + +@System +func UIRenderPreparing( + _ uiComponents: Extract< + Query + > +) { + uiComponents.wrappedValue.forEach { component in + + } +} + +func ExtractUIComponents( + _ uiComponents: Extract< + Query + > +) { + +} + +struct UIRenderNode: RenderNode { + func execute( + context: inout Context, + renderContext: AdaRender.RenderContext + ) async throws -> [AdaRender.RenderSlotValue] { + return [] + } +} + +struct UIRenderItem: RenderItem { + var sortKey: Int + var entity: AdaECS.Entity.ID + var drawPass: any AdaRender.DrawPass +} + +struct UIRenderDrawPass: DrawPass { + func render( + with renderEncoder: any AdaRender.RenderCommandEncoder, + world: AdaECS.World, + view: AdaECS.Entity, + item: UIRenderItem + ) throws { + + } +} diff --git a/Sources/AdaUI/Renderer2D.swift b/Sources/AdaUI/Renderer2D.swift index d304a2ffa..277db9777 100644 --- a/Sources/AdaUI/Renderer2D.swift +++ b/Sources/AdaUI/Renderer2D.swift @@ -8,6 +8,7 @@ //import AdaECS //import AdaAssets //import AdaText +//import AdaUtils //@_spi(Internal) import AdaRender //import Math // @@ -21,8 +22,6 @@ //@MainActor //class Renderer2D { // -// public static let shared = Renderer2D() -// // private var uniformSet: UniformBufferSet // // struct Data { @@ -55,245 +54,246 @@ // // TODO: (Vlad) Maybe we should split this code // // swiftlint:disable:next function_body_length // private init() { -// let device = RenderEngine.shared.renderDevice -// -// self.uniformSet = device.createUniformBufferSet() -// self.uniformSet.label = "Renderer2D_ViewUniform" -// self.uniformSet.initBuffers(for: GlobalViewUniform.self, binding: GlobalBufferIndex.viewUniform, set: 0) -// -// self.quadPosition = [ -// [-0.5, -0.5, 0.0, 1.0], -// [ 0.5, -0.5, 0.0, 1.0], -// [ 0.5, 0.5, 0.0, 1.0], -// [-0.5, 0.5, 0.0, 1.0] -// ] -// -// var quadIndices = [UInt32].init(repeating: 0, count: Self.maxIndecies) -// -// var offset: UInt32 = 0 -// for index in stride(from: 0, to: Self.maxIndecies, by: 6) { -// quadIndices[index + 0] = offset + 0 -// quadIndices[index + 1] = offset + 1 -// quadIndices[index + 2] = offset + 2 -// -// quadIndices[index + 3] = offset + 2 -// quadIndices[index + 4] = offset + 3 -// quadIndices[index + 5] = offset + 0 -// -// offset += 4 -// } -// -// let quadIndexBuffer = device.createIndexBuffer( -// format: .uInt32, -// bytes: &quadIndices, -// length: Self.maxIndecies -// ) -// quadIndexBuffer.label = "Renderer2D_QuadIndexBuffer" -// -// // Circle -// -// let circleShader = try! AssetsManager.loadSync( -// ShaderModule.self, -// at: "Shaders/circle.glsl", -// from: .engineBundle -// ) -// -// var piplineDesc = RenderPipelineDescriptor() -// piplineDesc.vertex = circleShader.asset.getShader(for: .vertex) -// piplineDesc.fragment = circleShader.asset.getShader(for: .fragment) -// piplineDesc.debugName = "Circle Pipeline" -// -// piplineDesc.vertexDescriptor.attributes.append([ -// .attribute(.vector4, name: "worldPosition"), -// .attribute(.vector4, name: "localPosition"), -// .attribute(.float, name: "thickness"), -// .attribute(.float, name: "fade"), -// .attribute(.vector4, name: "color"), -// ]) -// -// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride -// -// var attachment = ColorAttachmentDescriptor(format: .bgra8) -// attachment.isBlendingEnabled = true -// -// piplineDesc.colorAttachments = [attachment] -// -// let circlePipeline = device.createRenderPipeline(from: piplineDesc) -// -// let circleVertexBuffer = device.createVertexBuffer( -// length: MemoryLayout.stride * Self.maxVerticies, -// binding: 0 -// ) -// circleVertexBuffer.label = "Renderer2D_CircleVertexBuffer" -// -// self.circleData = Data( -// vertexBuffer: circleVertexBuffer, -// vertices: [], -// indeciesCount: 0, -// indexBuffer: quadIndexBuffer, -// renderPipeline: circlePipeline, -// textureSlots: [Texture2D].init(repeating: .whiteTexture, count: 16) -// ) -// -// // Quads -// -// piplineDesc.vertexDescriptor = VertexDescriptor() -// -// piplineDesc.debugName = "Quad Pipline" -// -// let quadShader = try! AssetsManager.loadSync( -// ShaderModule.self, -// at: "Shaders/quad.glsl", -// from: .engineBundle -// ) -// piplineDesc.vertex = quadShader.asset.getShader(for: .vertex) -// piplineDesc.fragment = quadShader.asset.getShader(for: .fragment) -// -// piplineDesc.vertexDescriptor.attributes.append([ -// .attribute(.vector4, name: "a_Position"), -// .attribute(.vector4, name: "a_Color"), -// .attribute(.vector2, name: "a_TexCoordinate"), -// .attribute(.int, name: "a_TexIndex") -// ]) -// -// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride -// -// let quadPipeline = device.createRenderPipeline(from: piplineDesc) -// -// let quadVertexBuffer = device.createVertexBuffer( -// length: MemoryLayout.stride * Self.maxVerticies, -// binding: 0 -// ) -// quadVertexBuffer.label = "Renderer2D_QuadVertexBuffer" -// -// self.quadData = Data( -// vertexBuffer: quadVertexBuffer, -// vertices: [], -// indeciesCount: 0, -// indexBuffer: quadIndexBuffer, -// renderPipeline: quadPipeline, -// textureSlots: [Texture2D].init(repeating: .whiteTexture, count: Self.maxTexturesPerBatch) -// ) -// -// // Lines -// -// piplineDesc.vertexDescriptor = VertexDescriptor() -// -// piplineDesc.debugName = "Lines Pipeline" -// -// let linesShader = try! AssetsManager.loadSync( -// ShaderModule.self, -// at: "Shaders/line.glsl", -// from: .engineBundle -// ) -// piplineDesc.vertex = linesShader.asset.getShader(for: .vertex) -// piplineDesc.fragment = linesShader.asset.getShader(for: .fragment) -// -// piplineDesc.vertexDescriptor.attributes.append([ -// .attribute(.vector3, name: "position"), -// .attribute(.vector4, name: "color"), -// .attribute(.float, name: "lineWidth"), -// ]) -// -// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride -// -// let linesPipeline = device.createRenderPipeline(from: piplineDesc) -// -// let linesVertexBuffer = device.createVertexBuffer( -// length: MemoryLayout.stride * Self.maxLineVertices, -// binding: 0 -// ) -// linesVertexBuffer.label = "Renderer2D_LinesVertexBuffer" -// -// var buffer: [Int32] = [Int32].init(repeating: 0, count: Self.maxLineIndices) -// -// for i in 0 ..< Self.maxLineIndices { -// buffer[i] = Int32(i) -// } -// -// let indexBuffer = device.createIndexBuffer( -// format: .uInt32, -// bytes: &buffer, -// length: Self.maxLineIndices -// ) -// indexBuffer.label = "Renderer2D_LinesIndexBuffer" -// -// self.lineData = Data( -// vertexBuffer: linesVertexBuffer, -// vertices: [], -// indeciesCount: 0, -// indexBuffer: indexBuffer, -// renderPipeline: linesPipeline, -// textureSlots: [Texture2D].init(repeating: .whiteTexture, count: 16) -// ) -// -// // Text -// -// let textIndexBuffer = device.createIndexBuffer( -// format: .uInt32, -// bytes: &quadIndices, -// length: Self.maxIndecies -// ) -// textIndexBuffer.label = "Renderer2D_TextIndexBuffer" -// -// piplineDesc.vertexDescriptor = VertexDescriptor() -// -// piplineDesc.debugName = "Text Pipeline" -// -// let textShader = try! AssetsManager.loadSync( -// ShaderModule.self, -// at: "Shaders/text.glsl", -// from: .engineBundle -// ) -// piplineDesc.vertex = textShader.asset.getShader(for: .vertex) -// piplineDesc.fragment = textShader.asset.getShader(for: .fragment) -// -// piplineDesc.vertexDescriptor.attributes.append([ -// .attribute(.vector4, name: "position"), -// .attribute(.vector4, name: "foregroundColor"), -// .attribute(.vector4, name: "outlineColor"), -// .attribute(.vector2, name: "textureCoordinate"), -// .attribute(.int, name: "textureIndex") -// ]) -// -// let textVertexBuffer = device.createVertexBuffer( -// length: MemoryLayout.stride * Self.maxVerticies, -// binding: 0 -// ) -// textVertexBuffer.label = "Renderer2D_QuadVertexBuffer" -// -// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride -// -// let textPipeline = device.createRenderPipeline(from: piplineDesc) -// -// self.textData = Data( -// vertexBuffer: textVertexBuffer, -// vertices: [], -// indeciesCount: 0, -// indexBuffer: textIndexBuffer, -// renderPipeline: textPipeline, -// textureSlots: [Texture2D].init(repeating: .whiteTexture, count: Self.maxTexturesPerBatch) -// ) +//// let device = RenderEngine.shared.renderDevice +//// +//// self.uniformSet = device.createUniformBufferSet() +//// self.uniformSet.label = "Renderer2D_ViewUniform" +//// self.uniformSet.initBuffers(for: GlobalViewUniform.self, binding: GlobalBufferIndex.viewUniform, set: 0) +//// +//// self.quadPosition = [ +//// [-0.5, -0.5, 0.0, 1.0], +//// [ 0.5, -0.5, 0.0, 1.0], +//// [ 0.5, 0.5, 0.0, 1.0], +//// [-0.5, 0.5, 0.0, 1.0] +//// ] +//// +//// var quadIndices = [UInt32].init(repeating: 0, count: Self.maxIndecies) +//// +//// var offset: UInt32 = 0 +//// for index in stride(from: 0, to: Self.maxIndecies, by: 6) { +//// quadIndices[index + 0] = offset + 0 +//// quadIndices[index + 1] = offset + 1 +//// quadIndices[index + 2] = offset + 2 +//// +//// quadIndices[index + 3] = offset + 2 +//// quadIndices[index + 4] = offset + 3 +//// quadIndices[index + 5] = offset + 0 +//// +//// offset += 4 +//// } +//// +//// let quadIndexBuffer = device.createIndexBuffer( +//// format: .uInt32, +//// bytes: &quadIndices, +//// length: Self.maxIndecies +//// ) +//// quadIndexBuffer.label = "Renderer2D_QuadIndexBuffer" +//// +//// // Circle +//// +//// let circleShader = try! AssetsManager.loadSync( +//// ShaderModule.self, +//// at: "Shaders/circle.glsl", +//// from: .engineBundle +//// ) +//// +//// var piplineDesc = RenderPipelineDescriptor() +//// piplineDesc.vertex = circleShader.asset.getShader(for: .vertex) +//// piplineDesc.fragment = circleShader.asset.getShader(for: .fragment) +//// piplineDesc.debugName = "Circle Pipeline" +//// +//// piplineDesc.vertexDescriptor.attributes.append([ +//// .attribute(.vector4, name: "worldPosition"), +//// .attribute(.vector4, name: "localPosition"), +//// .attribute(.float, name: "thickness"), +//// .attribute(.float, name: "fade"), +//// .attribute(.vector4, name: "color"), +//// ]) +//// +//// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride +//// +//// var attachment = ColorAttachmentDescriptor(format: .bgra8) +//// attachment.isBlendingEnabled = true +//// +//// piplineDesc.colorAttachments = [attachment] +//// +//// let circlePipeline = device.createRenderPipeline(from: piplineDesc) +//// +//// let circleVertexBuffer = device.createVertexBuffer( +//// length: MemoryLayout.stride * Self.maxVerticies, +//// binding: 0 +//// ) +//// circleVertexBuffer.label = "Renderer2D_CircleVertexBuffer" +//// +//// self.circleData = Data( +//// vertexBuffer: circleVertexBuffer, +//// vertices: [], +//// indeciesCount: 0, +//// indexBuffer: quadIndexBuffer, +//// renderPipeline: circlePipeline, +//// textureSlots: [Texture2D].init(repeating: .whiteTexture, count: 16) +//// ) +//// +//// // Quads +//// +//// piplineDesc.vertexDescriptor = VertexDescriptor() +//// +//// piplineDesc.debugName = "Quad Pipline" +//// +//// let quadShader = try! AssetsManager.loadSync( +//// ShaderModule.self, +//// at: "Shaders/quad.glsl", +//// from: .engineBundle +//// ) +//// piplineDesc.vertex = quadShader.asset.getShader(for: .vertex) +//// piplineDesc.fragment = quadShader.asset.getShader(for: .fragment) +//// +//// piplineDesc.vertexDescriptor.attributes.append([ +//// .attribute(.vector4, name: "a_Position"), +//// .attribute(.vector4, name: "a_Color"), +//// .attribute(.vector2, name: "a_TexCoordinate"), +//// .attribute(.int, name: "a_TexIndex") +//// ]) +//// +//// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride +//// +//// let quadPipeline = device.createRenderPipeline(from: piplineDesc) +//// +//// let quadVertexBuffer = device.createVertexBuffer( +//// length: MemoryLayout.stride * Self.maxVerticies, +//// binding: 0 +//// ) +//// quadVertexBuffer.label = "Renderer2D_QuadVertexBuffer" +//// +//// self.quadData = Data( +//// vertexBuffer: quadVertexBuffer, +//// vertices: [], +//// indeciesCount: 0, +//// indexBuffer: quadIndexBuffer, +//// renderPipeline: quadPipeline, +//// textureSlots: [Texture2D].init(repeating: .whiteTexture, count: Self.maxTexturesPerBatch) +//// ) +//// +//// // Lines +//// +//// piplineDesc.vertexDescriptor = VertexDescriptor() +//// +//// piplineDesc.debugName = "Lines Pipeline" +//// +//// let linesShader = try! AssetsManager.loadSync( +//// ShaderModule.self, +//// at: "Shaders/line.glsl", +//// from: .engineBundle +//// ) +//// piplineDesc.vertex = linesShader.asset.getShader(for: .vertex) +//// piplineDesc.fragment = linesShader.asset.getShader(for: .fragment) +//// +//// piplineDesc.vertexDescriptor.attributes.append([ +//// .attribute(.vector3, name: "position"), +//// .attribute(.vector4, name: "color"), +//// .attribute(.float, name: "lineWidth"), +//// ]) +//// +//// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride +//// +//// let linesPipeline = device.createRenderPipeline(from: piplineDesc) +//// +//// let linesVertexBuffer = device.createVertexBuffer( +//// length: MemoryLayout.stride * Self.maxLineVertices, +//// binding: 0 +//// ) +//// linesVertexBuffer.label = "Renderer2D_LinesVertexBuffer" +//// +//// var buffer: [Int32] = [Int32].init(repeating: 0, count: Self.maxLineIndices) +//// +//// for i in 0 ..< Self.maxLineIndices { +//// buffer[i] = Int32(i) +//// } +//// +//// let indexBuffer = device.createIndexBuffer( +//// format: .uInt32, +//// bytes: &buffer, +//// length: Self.maxLineIndices +//// ) +//// indexBuffer.label = "Renderer2D_LinesIndexBuffer" +//// +//// self.lineData = Data( +//// vertexBuffer: linesVertexBuffer, +//// vertices: [], +//// indeciesCount: 0, +//// indexBuffer: indexBuffer, +//// renderPipeline: linesPipeline, +//// textureSlots: [Texture2D].init(repeating: .whiteTexture, count: 16) +//// ) +//// +//// // Text +//// +//// let textIndexBuffer = device.createIndexBuffer( +//// format: .uInt32, +//// bytes: &quadIndices, +//// length: Self.maxIndecies +//// ) +//// textIndexBuffer.label = "Renderer2D_TextIndexBuffer" +//// +//// piplineDesc.vertexDescriptor = VertexDescriptor() +//// +//// piplineDesc.debugName = "Text Pipeline" +//// +//// let textShader = try! AssetsManager.loadSync( +//// ShaderModule.self, +//// at: "Shaders/text.glsl", +//// from: .engineBundle +//// ) +//// piplineDesc.vertex = textShader.asset.getShader(for: .vertex) +//// piplineDesc.fragment = textShader.asset.getShader(for: .fragment) +//// +//// piplineDesc.vertexDescriptor.attributes.append([ +//// .attribute(.vector4, name: "position"), +//// .attribute(.vector4, name: "foregroundColor"), +//// .attribute(.vector4, name: "outlineColor"), +//// .attribute(.vector2, name: "textureCoordinate"), +//// .attribute(.int, name: "textureIndex") +//// ]) +//// +//// let textVertexBuffer = device.createVertexBuffer( +//// length: MemoryLayout.stride * Self.maxVerticies, +//// binding: 0 +//// ) +//// textVertexBuffer.label = "Renderer2D_QuadVertexBuffer" +//// +//// piplineDesc.vertexDescriptor.layouts[0].stride = MemoryLayout.stride +//// +//// let textPipeline = device.createRenderPipeline(from: piplineDesc) +//// +//// self.textData = Data( +//// vertexBuffer: textVertexBuffer, +//// vertices: [], +//// indeciesCount: 0, +//// indexBuffer: textIndexBuffer, +//// renderPipeline: textPipeline, +//// textureSlots: [Texture2D].init(repeating: .whiteTexture, count: Self.maxTexturesPerBatch) +//// ) // } // -// static func beginDrawContext(for window: WindowRef, viewTransform: Transform3D) throws -> DrawContext { +// func beginDrawContext(for window: WindowRef, viewTransform: Transform3D) throws -> DrawContext { // let frameIndex = RenderEngine.shared.currentFrameIndex // // let uniform = Self.shared.uniformSet.getBuffer(binding: GlobalBufferIndex.viewUniform, set: 0, frameIndex: frameIndex) // uniform.setData(GlobalViewUniform(projectionMatrix: .identity, viewProjectionMatrix: .identity, viewMatrix: viewTransform)) // -// let currentDraw = try RenderEngine.shared.renderDevice.beginDraw( -// for: window, -// clearColor: .surfaceClearColor, -// loadAction: .load, -// storeAction: .store -// ) +// +//// let currentDraw = try RenderEngine.shared.renderDevice.beginDraw( +//// for: window, +//// clearColor: .surfaceClearColor, +//// loadAction: .load, +//// storeAction: .store +//// ) // let context = DrawContext(currentDraw: currentDraw, renderEngine: Self.shared, frameIndex: frameIndex) // context.startBatch() // return context // } // -// static func beginDrawContext(for camera: Camera, viewUniform: GlobalViewUniform) throws -> DrawContext { +// func beginDrawContext(for camera: Camera, viewUniform: GlobalViewUniform) throws -> DrawContext { // let frameIndex = RenderEngine.shared.currentFrameIndex // let device = RenderEngine.shared.renderDevice // @@ -341,13 +341,13 @@ //extension Renderer2D { // @MainActor // public class DrawContext { -// let currentDraw: DrawList +// let commandEncoder: RenderCommandEncoder // private var lineWidth: Float = 1 // private let renderEngine: Renderer2D // private let frameIndex: Int // -// init(currentDraw: DrawList, renderEngine: Renderer2D, frameIndex: Int) { -// self.currentDraw = currentDraw +// init(commandEncoder: RenderCommandEncoder, renderEngine: Renderer2D, frameIndex: Int) { +// self.commandEncoder = commandEncoder // self.renderEngine = renderEngine // self.frameIndex = frameIndex // } @@ -403,11 +403,11 @@ // } // // public func pushDebugName(_ name: String) { -// self.currentDraw.pushDebugName(name) +// self.commandEncoder.pushDebugName(name) // } // // public func setLineWidth(_ width: Float) { -// self.currentDraw.setLineWidth(width) +// self.commandEncoder.setLineWidth(width) // } // // public func drawCircle( diff --git a/Sources/AdaUI/Scenes/WindowGroup.swift b/Sources/AdaUI/Scenes/WindowGroup.swift index 37e5e0b48..8ca24eeb6 100644 --- a/Sources/AdaUI/Scenes/WindowGroup.swift +++ b/Sources/AdaUI/Scenes/WindowGroup.swift @@ -35,29 +35,24 @@ struct InitialContainerView: Resource { } @System -struct WindowGroupSystem { - - @LocalIsolated private var isAllocated = false - - init(world: World) { } - - func update(context: inout UpdateContext) { - if isAllocated { - return - } - guard - let resource = context.world.getResource(PrimaryWindow.self), - let containerView = context.world.getResource(InitialContainerView.self) - else { - return - } - - context.taskGroup.addTask { @MainActor in - let gameSceneView = containerView.view - gameSceneView.autoresizingRules = [.flexibleWidth, .flexibleHeight] - resource.window.addSubview(gameSceneView) - - self.isAllocated = true - } +@MainActor +func WindowGroupUpdate( + _ context: WorldUpdateContext, + _ isAllocated: Local = false +) { + if isAllocated.wrappedValue { + return } + guard + let resource = context.world.getResource(PrimaryWindow.self), + let containerView = context.world.getResource(InitialContainerView.self) + else { + return + } + + let view = containerView.view + view.autoresizingRules = [.flexibleWidth, .flexibleHeight] + resource.window.addSubview(view) + + isAllocated.wrappedValue = true } diff --git a/Sources/AdaUI/UI/UIComponent.swift b/Sources/AdaUI/UI/UIComponent.swift index 25d4a9425..cf78fc0a4 100644 --- a/Sources/AdaUI/UI/UIComponent.swift +++ b/Sources/AdaUI/UI/UIComponent.swift @@ -6,6 +6,7 @@ // import AdaECS +import AdaUtils /// - Warning: Work in progress component @Component diff --git a/Sources/AdaUI/UIImageView.swift b/Sources/AdaUI/UIImageView.swift index cc7630643..02663bc91 100644 --- a/Sources/AdaUI/UIImageView.swift +++ b/Sources/AdaUI/UIImageView.swift @@ -9,14 +9,6 @@ import AdaUtils import AdaRender import Math -/// The render mode of the image view. -public enum ImageRenderMode { - /// The original render mode. - case original - /// The template render mode. - case template -} - /// A view that displays a single image or a sequence of animated images in your interface. public class UIImageView: UIView { diff --git a/Sources/AdaUI/UILayer.swift b/Sources/AdaUI/UILayer.swift index bb29f0219..571fb12f9 100644 --- a/Sources/AdaUI/UILayer.swift +++ b/Sources/AdaUI/UILayer.swift @@ -65,11 +65,11 @@ extension UIGraphicsContext { debugLabel: layer.debugLabel.flatMap { "Layer \($0)" } ) - var context = UIGraphicsContext(texture: renderTexture) - context.environment = self.environment - context.beginDraw(in: layer.frame.size, scaleFactor: 1) - drawBlock(&context) - context.commitDraw() +// var context = UIGraphicsContext(texture: renderTexture) +// context.environment = self.environment +// context.beginDraw(in: layer.frame.size, scaleFactor: 1) +// drawBlock(&context) +// context.commitDraw() return renderTexture } diff --git a/Sources/AdaUI/UIPlugin.swift b/Sources/AdaUI/UIPlugin.swift deleted file mode 100644 index 93a208b79..000000000 --- a/Sources/AdaUI/UIPlugin.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// UIPlugin.swift -// AdaEngine -// -// Created by vladislav.prusakov on 19.08.2024. -// - -import AdaApp -import AdaECS -import AdaUtils -import Math - -public struct UIPlugin: Plugin { - - public init() { } - - public func setup(in app: AppWorlds) { - UIComponent.registerComponent() - - app - .addSystem(GraphicsContextInitializedSystem.self) - .addSystem(UIComponentSystem.self) - } -} - -public struct WindowPlugin: Plugin { - let primaryWindow: UIWindow? - - public init(primaryWindow: UIWindow? = nil) { - self.primaryWindow = primaryWindow - } - - public func setup(in app: AppWorlds) { - guard let windowSettings = app.getResource(WindowSettings.self) else { - return - } - - if let primaryWindow { - primaryWindow.showWindow(makeFocused: true) - app.insertResource(PrimaryWindow(window: primaryWindow)) - } else { - let window = UIWindow() - window.title = windowSettings.title ?? "App" - window.minSize = windowSettings.minimumSize - window.frame = Rect(origin: .zero, size: windowSettings.minimumSize) - window.setWindowMode( - windowSettings.windowMode == .fullscreen ? .fullscreen : .windowed - ) - window.showWindow(makeFocused: true) - app.insertResource(PrimaryWindow(window: window)) - } - } -} - -public struct PrimaryWindow: Resource { - public let window: UIWindow -} - -@PlainSystem -func GraphicsContextInitialized( - _ world: Ref, - _ kek: ResQuery -) { - -} diff --git a/Sources/AdaUI/UIView.swift b/Sources/AdaUI/UIView.swift index 3cbe89da3..b8766e507 100644 --- a/Sources/AdaUI/UIView.swift +++ b/Sources/AdaUI/UIView.swift @@ -321,11 +321,9 @@ open class UIView { guard self.isInteractionEnabled && !self.isHidden else { return nil } - if !self.point(inside: point, with: event) { return nil } - if self.subviews.isEmpty { return self } @@ -395,6 +393,8 @@ open class UIView { /// - Parameter event: The mouse event. open func onMouseEvent(_ event: MouseEvent) { } + open func onKeyPressed(_ event: Set) { } + /// Called when the event is received. /// /// - Parameter event: The event. @@ -404,8 +404,8 @@ open class UIView { self.onMouseEvent(event) case is TouchEvent: let window = self.window?.id - let touches = Input.shared.touches.filter({ $0.window == window }) - self.onTouchesEvent(touches) +// let touches = Input.shared.touches.filter({ $0.window == window }) +// self.onTouchesEvent(touches) default: return } @@ -428,17 +428,19 @@ open class UIView { /// - Parameter event: The event to find the first responder for. /// - Returns: The first responder. func findFirstResponder(for event: any InputEvent) -> UIView? { - let responder: UIView? - - switch event { + let responder: UIView? = switch event { case let event as MouseEvent: - let point = convert(event.mousePosition, to: self) - responder = self.hitTest(point, with: event) + self.hitTest( + convert(event.mousePosition, to: self), + with: event + ) case let event as TouchEvent: - let point = event.location - responder = self.hitTest(point, with: event) + self.hitTest( + event.location, + with: event + ) default: - return nil + nil } if responder?.canRespondToAction(event) == false { diff --git a/Sources/AdaUI/UIWindow.swift b/Sources/AdaUI/UIWindow.swift index fabe77e18..dd684ff2c 100644 --- a/Sources/AdaUI/UIWindow.swift +++ b/Sources/AdaUI/UIWindow.swift @@ -30,6 +30,7 @@ open class UIWindow: UIView { public var windowManager: UIWindowManager { UIWindowManager.shared } + @_spi(Internal) public var systemWindow: SystemWindow? internal let eventManager = EventManager() diff --git a/Sources/AdaUI/UIWindowManager.swift b/Sources/AdaUI/UIWindowManager.swift index 55e289e15..427bc1cae 100644 --- a/Sources/AdaUI/UIWindowManager.swift +++ b/Sources/AdaUI/UIWindowManager.swift @@ -9,6 +9,7 @@ import AdaRender import AdaUtils import Math +import AdaECS /// Base protocol describes platform specific window. @MainActor @@ -36,29 +37,10 @@ open class UIWindowManager { /// Contains active window if available. public private(set) var activeWindow: UIWindow? - public init() { } - - /// Called each frame to update windows. @_spi(Internal) - public func update(_ deltaTime: AdaUtils.TimeInterval) async { - for window in self.windows { - let menuBuilder = self.menuBuilder(for: window) - menuBuilder?.updateIfNeeded() - - for event in Input.shared.eventsPool where event.window == window.id { - window.sendEvent(event) - } - - await window.internalUpdate(deltaTime) + public var inputRef: Ref? - if window.canDraw { - var context = UIGraphicsContext(window: window) - context.beginDraw(in: window.frame.size, scaleFactor: 1) - window.draw(with: context) - context.commitDraw() - } - } - } + public init() { } open func menuBuilder(for window: UIWindow) -> UIMenuBuilder? { return nil @@ -142,7 +124,7 @@ open class UIWindowManager { } // Destory window from render window - try? RenderEngine.shared.destroyWindow(.windowId(window.id)) + try? RenderEngine.shared.destroyWindow(window.id) self.windows.remove(at: index) window.windowDidDisappear() diff --git a/Sources/AdaUtils/Atomic.swift b/Sources/AdaUtils/Atomic.swift index 8d81b31df..18449d941 100644 --- a/Sources/AdaUtils/Atomic.swift +++ b/Sources/AdaUtils/Atomic.swift @@ -5,12 +5,16 @@ // Created by v.prusakov on 4/30/24. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// A property wrapper that allows you to isolate a value with a lock. @propertyWrapper @dynamicMemberLookup -public final class LocalIsolated: @unchecked Sendable { +public final class LocalIsolated { /// The lock-isolated value. public var wrappedValue: Value { @@ -54,6 +58,8 @@ public final class LocalIsolated: @unchecked Sendable { } } +extension LocalIsolated: @unchecked Sendable where Value: Sendable {} + extension NSRecursiveLock { @inlinable @discardableResult @_spi(Internal) diff --git a/Sources/AdaUtils/Cancellable.swift b/Sources/AdaUtils/Cancellable.swift index 6dd0a39bc..4955af842 100644 --- a/Sources/AdaUtils/Cancellable.swift +++ b/Sources/AdaUtils/Cancellable.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 7/3/22. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// A protocol indicating that an activity or action supports cancellation. /// diff --git a/Sources/AdaUtils/Codable/AnyCodable.swift b/Sources/AdaUtils/Codable/AnyCodable.swift index 40d26851c..f52b73a72 100644 --- a/Sources/AdaUtils/Codable/AnyCodable.swift +++ b/Sources/AdaUtils/Codable/AnyCodable.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 5/21/25. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif // Taken from https://github.com/Flight-School/AnyCodable @@ -141,4 +145,4 @@ extension AnyCodable: Hashable { break } } -} \ No newline at end of file +} diff --git a/Sources/AdaUtils/Codable/AnyDecodable.swift b/Sources/AdaUtils/Codable/AnyDecodable.swift index d0fcf8cdd..a7d60dc76 100644 --- a/Sources/AdaUtils/Codable/AnyDecodable.swift +++ b/Sources/AdaUtils/Codable/AnyDecodable.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 5/21/25. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif @frozen public struct AnyDecodable: Decodable { public let value: Any @@ -161,4 +165,4 @@ extension AnyDecodable: Hashable { break } } -} \ No newline at end of file +} diff --git a/Sources/AdaUtils/Codable/AnyEncodable.swift b/Sources/AdaUtils/Codable/AnyEncodable.swift index 88dc0b774..4ae09353a 100644 --- a/Sources/AdaUtils/Codable/AnyEncodable.swift +++ b/Sources/AdaUtils/Codable/AnyEncodable.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 5/21/25. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif @frozen public struct AnyEncodable: Encodable { public let value: Any @@ -86,7 +90,7 @@ extension _AnyEncodable { #if canImport(Foundation) private func encode(nsnumber: NSNumber, into container: inout SingleValueEncodingContainer) throws { - switch Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) { + switch unsafe Character(Unicode.Scalar(UInt8(nsnumber.objCType.pointee))) { case "B": try container.encode(nsnumber.boolValue) case "c": @@ -266,4 +270,4 @@ extension AnyEncodable: Hashable { break } } -} \ No newline at end of file +} diff --git a/Sources/AdaUtils/Codable/ExportCodable.swift b/Sources/AdaUtils/Codable/ExportCodable.swift index 4cbf5ce31..3128633e7 100644 --- a/Sources/AdaUtils/Codable/ExportCodable.swift +++ b/Sources/AdaUtils/Codable/ExportCodable.swift @@ -34,6 +34,6 @@ public struct CodingName: CodingKey { } public extension CodingName { - nonisolated(unsafe) static var editor = CodingName(stringValue: "_editor") - nonisolated(unsafe) static var value = CodingName(stringValue: "_value") + static let editor = CodingName(stringValue: "_editor") + static let value = CodingName(stringValue: "_value") } diff --git a/Sources/AdaUtils/Collections/Collection+SwapAndRemove.swift b/Sources/AdaUtils/Collections/Collection+SwapAndRemove.swift new file mode 100644 index 000000000..d57dc7b8a --- /dev/null +++ b/Sources/AdaUtils/Collections/Collection+SwapAndRemove.swift @@ -0,0 +1,56 @@ +// +// Sequence+SwapAndRemove.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 16.11.2025. +// + +public extension Array { + /// Removes an element from the array and returns it. + /// + /// # Examples + /// + /// ```swift + /// var array = Array["foo", "bar", "baz", "qux"]; + /// + /// assert(array.swapRemove(at: 1) == "bar") + /// assert(array == ["foo", "qux", "baz"]) + /// + /// assert(array.swapRemove(at: 0) == "foo") + /// assert(array == ["baz", "qux"]) + /// ``` + @discardableResult + mutating func swapRemove(at index: Int) -> Element { + let length = self.count + precondition(index <= length, "swapRemove index is \(index) should be < len (is \(length))") + let tmp = self[index] + self[index] = self[length - 1] + self[length - 1] = tmp + return self.removeLast() + } +} + +public extension ContiguousArray { + /// Removes an element from the array and returns it. + /// + /// # Examples + /// + /// ```swift + /// var array = ContiguousArray["foo", "bar", "baz", "qux"]; + /// + /// assert(array.swapRemove(at: 1) == "bar") + /// assert(array == ["foo", "qux", "baz"]) + /// + /// assert(array.swapRemove(at: 0) == "foo") + /// assert(array == ["baz", "qux"]) + /// ``` + @discardableResult + mutating func swapRemove(at index: Int) -> Element { + let length = self.count + precondition(index <= length, "swapRemove index is \(index) should be < len (is \(length))") + let tmp = self[index] + self[index] = self[length - 1] + self[length - 1] = tmp + return self.removeLast() + } +} diff --git a/Sources/AdaUtils/ConcurrentSequence.swift b/Sources/AdaUtils/Collections/ConcurrentSequence.swift similarity index 98% rename from Sources/AdaUtils/ConcurrentSequence.swift rename to Sources/AdaUtils/Collections/ConcurrentSequence.swift index 2028a228a..a400f3db8 100644 --- a/Sources/AdaUtils/ConcurrentSequence.swift +++ b/Sources/AdaUtils/Collections/ConcurrentSequence.swift @@ -25,6 +25,7 @@ public extension Sequence { public extension ConcurrentSequence { /// Iterate over all elements in sequence and create task for each. + @concurrent func forEach( _ operation: @escaping @Sendable (Element) async -> Void ) async where Element: Sendable { diff --git a/Sources/AdaUtils/Collections/FixedArray.swift b/Sources/AdaUtils/Collections/FixedArray.swift index a5db0e091..293d98253 100644 --- a/Sources/AdaUtils/Collections/FixedArray.swift +++ b/Sources/AdaUtils/Collections/FixedArray.swift @@ -9,8 +9,8 @@ /// Create a fixed sized array on a heap. @frozen -public struct FixedArray: Sequence, RandomAccessCollection { - +@safe +public struct FixedArray: @unsafe Sequence, @unsafe RandomAccessCollection { public typealias Element = T? public typealias Index = Int @@ -29,7 +29,7 @@ public struct FixedArray: Sequence, RandomAccessCollection { // swiftlint:disable:next empty_count precondition(count > 0, "Can't allocate array with 0 elements.") self.buffer = Buffer(count: count) - self.buffer.pointer.update(repeating: repeating) + unsafe self.buffer.pointer.update(repeating: repeating) } @inline(__always) @@ -39,7 +39,7 @@ public struct FixedArray: Sequence, RandomAccessCollection { fatalError("Index out of range") } - return self.buffer.pointer[index] + return unsafe self.buffer.pointer[index] } set { @@ -49,7 +49,7 @@ public struct FixedArray: Sequence, RandomAccessCollection { self._ensureUnique() - self.buffer.pointer[index] = newValue + unsafe self.buffer.pointer[index] = newValue } } @@ -57,36 +57,36 @@ public struct FixedArray: Sequence, RandomAccessCollection { @inline(__always) public func makeIterator() -> UnsafeMutableBufferPointer.Iterator { - return self.buffer.pointer.makeIterator() + return unsafe self.buffer.pointer.makeIterator() } // MARK: - Collection @inline(__always) public var count: Int { - return self.buffer.pointer.count + return unsafe self.buffer.pointer.count } @inline(__always) public var startIndex: Index { - return self.buffer.pointer.startIndex + return unsafe self.buffer.pointer.startIndex } @inline(__always) public var endIndex: Index { - return self.buffer.pointer.endIndex + return unsafe self.buffer.pointer.endIndex } @inline(__always) public func index(after i: Index) -> Index { - return self.buffer.pointer.index(after: i) + return unsafe self.buffer.pointer.index(after: i) } /// Remove all elements and replace them by nil. @inline(__always) public mutating func removeAll() { self._ensureUnique() - self.buffer.pointer.update(repeating: nil) + unsafe self.buffer.pointer.update(repeating: nil) } /// Ensures that the sparse data storage buffer is uniquely referenced, @@ -109,14 +109,14 @@ extension FixedArray: Equatable where T: Equatable { return false } - return lhs.buffer.pointer.elementsEqual(rhs.buffer.pointer) + return unsafe lhs.buffer.pointer.elementsEqual(rhs.buffer.pointer) } } extension FixedArray: Hashable where T: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(self.count) - for element in self { + for unsafe element in self { hasher.combine(element) } } @@ -124,7 +124,7 @@ extension FixedArray: Hashable where T: Hashable { extension FixedArray: CustomStringConvertible { public var description: String { - let values: [String] = self.map { + let values: [String] = unsafe self.map { guard let value = $0 else { return "nil" } @@ -143,7 +143,7 @@ extension FixedArray: Encodable where T: Encodable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.count, forKey: .length) - let values = Array(self.buffer.pointer) + let values = unsafe Array(self.buffer.pointer) try container.encode(values, forKey: .values) } } @@ -156,7 +156,7 @@ extension FixedArray: Decodable where T: Codable { self.buffer = Buffer(count: length) for (index, value) in values.enumerated() { - self.buffer.pointer[index] = value + unsafe self.buffer.pointer[index] = value } } } @@ -172,25 +172,27 @@ extension FixedArray { extension FixedArray { @usableFromInline + @safe internal final class Buffer: @unchecked Sendable { let pointer: UnsafeMutableBufferPointer init(count: Int) { - self.pointer = UnsafeMutableBufferPointer.allocate(capacity: count) - self.pointer.initialize(repeating: nil) + unsafe self.pointer = UnsafeMutableBufferPointer.allocate(capacity: count) + unsafe self.pointer.initialize(repeating: nil) } func moveMemory(to destination: UnsafeMutableBufferPointer) { - self.pointer.baseAddress?.moveUpdate(from: destination.baseAddress!, count: self.pointer.count) + unsafe self.pointer.baseAddress?.moveUpdate(from: destination.baseAddress!, count: self.pointer.count) } deinit { - pointer.deallocate() + unsafe pointer.deinitialize() + unsafe pointer.deallocate() } static func buffer(count: Int, contentsOf buffer: Buffer) -> Buffer { let newBuffer = Buffer(count: count) - buffer.moveMemory(to: newBuffer.pointer) + unsafe buffer.moveMemory(to: newBuffer.pointer) return newBuffer } } diff --git a/Sources/AdaUtils/Collections/ManagedArray.swift b/Sources/AdaUtils/Collections/ManagedArray.swift new file mode 100644 index 000000000..d4f1ddf1f --- /dev/null +++ b/Sources/AdaUtils/Collections/ManagedArray.swift @@ -0,0 +1,162 @@ +// +// ManagedArray.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 16.06.2025. +// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +public struct ManagedArray: @unchecked Sendable where Element: ~Copyable { + + struct Header { + var capacity: Int + var count: Int = 0 + } + + private var buffer: ManagedBuffer + + public var startIndex: Int { + return 0 + } + + public var endIndex: Int { + self.buffer.header.count + } + + public var count: Int { + self.buffer.header.count + } + + public init() { + self.buffer = ManagedBuffer.create( + minimumCapacity: 16, + makingHeaderWith: { _ in + Header(capacity: 16) + } + ) + } + + public init(count: Int) { + precondition(count > 0) + self.buffer = ManagedBuffer.create( + minimumCapacity: count * MemoryLayout.stride, + makingHeaderWith: { _ in + Header(capacity: count * MemoryLayout.stride, count: 0) + } + ) + } + +// public mutating func insert(_ element: consuming Element, at index: Int) { +// precondition(index >= 0 && index <= self.buffer.header.count) +// if buffer.header.count == buffer.header.capacity { +// increaseCapacity(to: buffer.header.capacity > 0 ? buffer.header.capacity * 2 : 8) +// } +// self.buffer.withUnsafeMutablePointers { header, pointer in +// header.pointee.count += 1 +// pointer.advanced(by: index * MemoryLayout.stride) +// .initialize(to: element) +// } +// } + + public mutating func append(_ element: consuming Element) { +// insert(element, at: buffer.header.count) + } + + public mutating func increaseCapacity(to capacity: Int) { + precondition(self.buffer.header.capacity < capacity) + let oldBuffer = self.buffer + self.buffer = ManagedBuffer.create( + minimumCapacity: capacity, + makingHeaderWith: { _ in + Header(capacity: capacity, count: oldBuffer.header.count) + } + ) + unsafe self.buffer.withUnsafeMutablePointers { (header, elements) in + unsafe oldBuffer.withUnsafeMutablePointers { oldHeader, oldPtr in + unsafe elements.moveUpdate(from: oldPtr, count: oldHeader.pointee.count) + } + } + } + + @inline(__always) + public func reborrow(at index: Int, _ block: (inout Element) -> U) -> U { + unsafe self.buffer.withUnsafeMutablePointerToElements { ptr in + unsafe block(&ptr[index]) + } + } + + @discardableResult + public mutating func remove(at index: Int) -> Element { + precondition(index >= 0 && index < buffer.header.count) + return unsafe buffer.withUnsafeMutablePointers { header, pointer in + let nextToMove = unsafe header.pointee.count - index + let place = unsafe pointer.advanced(by: index * MemoryLayout.stride) + let value = unsafe place.move() + let nextPtr = unsafe place.successor() + unsafe place.moveUpdate(from: nextPtr, count: nextToMove) + unsafe header.pointee.count -= 1 + return value + } +// if index < self.header.count - 1 { +// baseAddress +// .advanced(by: MemoryLayout.stride * index) +// .moveInitialize(from: baseAddress.advanced(by: MemoryLayout.stride * index + 1), count: self.header.count - 1 - index) +// } + } + + public func getPointer(at index: Int) -> UnsafeMutablePointer { + + // self.buffer.pointer + // .baseAddress! + // .advanced(by: MemoryLayout.stride * index) + fatalErrorMethodNotImplemented() + } + + public consuming func take(at index: Int) -> Element { + fatalErrorMethodNotImplemented() +// precondition(index >= 0 && index < buffer.header.count) +// return self.buffer.withUnsafeMutablePointerToElements { pointer in +// pointer[index].move() +// } + } + + public func forEach(_ body: (borrowing Element) -> Void) { + unsafe buffer.withUnsafeMutablePointers { header, pointer in + for index in 0..(_ transform: (borrowing Element) -> T) -> [T] { + var result: [T] = [] + result.reserveCapacity(self.count) + unsafe buffer.withUnsafeMutablePointers { header, pointer in + for index in 0.. Void + ) async rethrows { + for element in self { + try await body(element) + } + } +} diff --git a/Sources/AdaUtils/Collections/SparseArray.swift b/Sources/AdaUtils/Collections/SparseArray.swift index 00da93ea6..6b9a9e5ae 100644 --- a/Sources/AdaUtils/Collections/SparseArray.swift +++ b/Sources/AdaUtils/Collections/SparseArray.swift @@ -6,19 +6,18 @@ // /// Fast collection O(1) for insertion and deletion, but slow for resizing and iterating. -@frozen public struct SparseArray { - +@frozen +public struct SparseArray { public typealias Index = Int - - @usableFromInline - internal var values: [Element?] - - @inline(__always) + + public var values: [Element?] + + @inlinable public init(capacity: Int) { self.values = [Element?].init(repeating: nil, count: capacity) } - @inline(__always) + @inlinable public init(_ sequence: T) where T.Element == Element { self.values = [Element?].init(repeating: nil, count: sequence.underestimatedCount) @@ -29,14 +28,12 @@ } extension SparseArray { - - @inline(__always) + @inlinable public subscript(_ index: Index) -> Element? { get { - precondition(index < self.count, "Index out of range") + precondition(index < self.underestimatedCount, "Index out of range") return self.values[index] } - mutating set { self.insert(newValue, at: index) } @@ -48,13 +45,19 @@ extension SparseArray { /// capacity is preserved. The default is `false`. /// /// - Complexity: O(`count`) - @inline(__always) + @inlinable public mutating func removeAll(keepingCapacity: Bool = false) { - self.values.removeAll(keepingCapacity: keepingCapacity) + if keepingCapacity { + for index in 0 ..< self.values.count { + self.values[index] = nil + } + } else { + self.values.removeAll() + } } /// - Complexity: O(1) - @inline(__always) + @inlinable @discardableResult public mutating func remove(at index: Index) -> Element? { let element = self.values[index] @@ -62,66 +65,81 @@ extension SparseArray { return element } - @inline(__always) + @inlinable @discardableResult public mutating func removeLast() -> Element? { - return self.remove(at: self.count - 1) + guard let index = self.values.lastIndex(where: { $0 != nil }) else { + return nil + } + return remove(at: index) } - @inline(__always) + @inlinable public mutating func insert(_ element: Element?, at index: Index) { - if index >= self.count { - values.reserveCapacity(index + 1) - values.append(nil) - } - self.values[index] = element } - @inline(__always) + @inlinable public mutating func append(_ element: Element) { + if count >= values.count { + values.append( + contentsOf: [Element?].init(repeating: nil, count: 16) + ) + } self.insert(element, at: self.count) } } extension SparseArray: ExpressibleByArrayLiteral { - @inline(__always) + @inlinable public init(arrayLiteral elements: Element...) { self = SparseArray(elements) } } extension SparseArray: Sequence { - + @inlinable + public func index(after i: Int) -> Int { + self.values.index(after: i) + } + + @inlinable + public var startIndex: Int { + self.values.startIndex + } + + @inlinable + public var endIndex: Int { + self.values.endIndex + } + /// - Complexity: O(1) - @inline(__always) + @inlinable public var underestimatedCount: Int { return self.values.underestimatedCount } - - /// - Complexity: O(1) - @inline(__always) + + /// - Complexity: O(n) + /// - Returns: Count of not null values. + @inlinable public var count: Int { - return self.values.count + return self.values.count(where: { $0 != nil }) } - /// - Complexity: O(1) - @inline(__always) + /// - Complexity: O(n) + @inlinable public var isEmpty: Bool { - return self.values.isEmpty + return self.count == 0 } - - @inline(__always) + public func makeIterator() -> Iterator { - return Iterator(values: self.values) + Iterator(values: self.values) } - - @frozen + public struct Iterator: IteratorProtocol { - - var pointer: Int = -1 + private var pointer: Int = -1 let values: [Element?] - + init(values: [Element?]) { self.values = values } diff --git a/Sources/AdaUtils/Collections/SparseSet.swift b/Sources/AdaUtils/Collections/SparseSet.swift new file mode 100644 index 000000000..b594ec1b9 --- /dev/null +++ b/Sources/AdaUtils/Collections/SparseSet.swift @@ -0,0 +1,197 @@ +// +// SparseSet.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 15.11.2025. +// + +@frozen +public struct SparseSet { + public typealias Index = Int + public typealias Element = Value + public typealias DenseValue = (key: Key, value: Value) + + @usableFromInline + var dense: ContiguousArray + @usableFromInline + var sparse: [Key: Index] + + public init() { + self.dense = [] + self.sparse = [:] + } +} + +public extension SparseSet { + @inlinable + var values: ContiguousArray { + self.dense + } + + @inlinable + func firstIndex(for key: Key) -> Index? { + guard let index = sparse[key], index < count else { + return nil + } + return index + } + + @inlinable + func firstValue(for key: Key) -> Value? { + guard let index = firstIndex(for: key) else { + return nil + } + let value = dense[index] + assert(value.key == key, "Stored value has different key") + return value.value + } + + @inlinable + func contains(_ key: Key) -> Bool { + self.firstIndex(for: key) != nil + } + + @discardableResult + @inlinable + mutating func insert(_ value: Value, for key: Key) -> DenseValue { + let newPair = (key, value) + if let index = firstIndex(for: key) { + dense[index] = newPair + return newPair + } + + let index = dense.count + dense.append(newPair) + sparse[key] = index + return newPair + } + + @discardableResult + @inlinable + mutating func remove(for key: Key) -> DenseValue? { + guard let index = firstIndex(for: key) else { + return nil + } + + let removed = swapAndRemove(at: index) + if !dense.isEmpty, index < dense.count { + let swappedElement = dense[index] + sparse[swappedElement.key] = index + } + sparse[key] = nil + return removed + } + + @inlinable + mutating func removeAll(keepingCapacity: Bool = false) { + self.dense.removeAll(keepingCapacity: keepingCapacity) + self.sparse.removeAll(keepingCapacity: keepingCapacity) + } + + @inlinable + subscript(_ key: Key) -> Value? { + _read { + yield firstValue(for: key) + } + set { + if let newValue { + insert(newValue, for: key) + } else { + remove(for: key) + } + } + } + + @inlinable + subscript(_ key: Key, default value: Value) -> Value? { + _read { + yield firstValue(for: key) ?? value + } + set { + if let newValue { + insert(newValue, for: key) + } else { + remove(for: key) + } + } + } +} + +extension SparseSet { + @inlinable + mutating func swapAndRemove(at index: Index) -> DenseValue? { + dense.swapAt(index, dense.count - 1) + return dense.removeLast() + } +} + +extension SparseSet: Sequence { + @inlinable + public func makeIterator() -> SparseSetIterator { + SparseSetIterator(collection: dense) + } + + @inlinable + public var isEmpty: Bool { + dense.isEmpty + } + + @inlinable + public var count: Int { + dense.count + } + + public struct SparseSetIterator: IteratorProtocol { + @usableFromInline + var iterator: IndexingIterator> + + @usableFromInline + init(collection: ContiguousArray) { + iterator = collection.makeIterator() + } + + @inlinable + public mutating func next() -> Element? { + iterator.next()?.value + } + } +} + +extension SparseSet: Sendable where Value: Sendable, Key: Sendable {} + +extension SparseSet: Codable where Value: Codable, Key: Codable { + enum CodingKeys: CodingKey { + case dense, sparse + } + + private struct CodableDenseValue: Codable { + let key: Key + let value: Value + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let denseValues = try container.decode(Array.self, forKey: .dense) + self.sparse = try container.decode([Key: Index].self, forKey: .sparse) + self.dense = ContiguousArray(denseValues.map { ($0.key, $0.value) }) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(sparse, forKey: .sparse) + try container.encode( + dense.map { CodableDenseValue(key: $0.key, value: $0.value) }, + forKey: .dense + ) + } +} + +extension SparseSet: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (Key, Value)...) { + var set = SparseSet() + for (key, value) in elements { + set.insert(value, for: key) + } + self = set + } +} diff --git a/Sources/AdaUtils/Events/EventManager.swift b/Sources/AdaUtils/Events/EventManager.swift index 224eb8114..f9af41e03 100644 --- a/Sources/AdaUtils/Events/EventManager.swift +++ b/Sources/AdaUtils/Events/EventManager.swift @@ -9,12 +9,13 @@ public protocol Event: Sendable { } /// An object on which events can be published and subscribed. -public final class EventManager { +public final class EventManager: @unchecked Sendable { - nonisolated(unsafe) public static let `default`: EventManager = EventManager() + public static let `default`: EventManager = EventManager() public init() {} - + + @LocalIsolated private var subscribers: [ObjectIdentifier : WeakSet] = [:] public func subscribe( diff --git a/Sources/AdaUtils/FileSystem/FileSystem.swift b/Sources/AdaUtils/FileSystem/FileSystem.swift index 274396706..c407b5217 100644 --- a/Sources/AdaUtils/FileSystem/FileSystem.swift +++ b/Sources/AdaUtils/FileSystem/FileSystem.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 1/20/23. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif // TODO: Should we use this instead of NSFileManager thats dilivered by SwiftFoundation? diff --git a/Sources/AdaUtils/FileSystem/FoundationFileSystem.swift b/Sources/AdaUtils/FileSystem/FoundationFileSystem.swift index 3aaa28131..42a1b750a 100644 --- a/Sources/AdaUtils/FileSystem/FoundationFileSystem.swift +++ b/Sources/AdaUtils/FileSystem/FoundationFileSystem.swift @@ -7,7 +7,11 @@ #if os(macOS) || os(iOS) || os(tvOS) || os(Linux) || os(Windows) || os(Android) +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif final class FoundationFileSystem: FileSystem, @unchecked Sendable { diff --git a/Sources/AdaUtils/FileWatcher.swift b/Sources/AdaUtils/FileWatcher.swift index fc70e73c2..8d34f2d93 100644 --- a/Sources/AdaUtils/FileWatcher.swift +++ b/Sources/AdaUtils/FileWatcher.swift @@ -9,9 +9,12 @@ // Take from https://github.com/swiftlang/swift-tools-support-core/blob/main/Sources/TSCUtility/FSWatch.swift -import Foundation import Dispatch +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif #if os(Windows) import WinSDK #endif @@ -762,10 +765,10 @@ private func callback( eventFlags: UnsafePointer, eventIds: UnsafePointer ) { - let eventStream = unsafeBitCast(clientCallBackInfo, to: FSEventStream.self) + let eventStream = unsafe unsafeBitCast(clientCallBackInfo, to: FSEventStream.self) // We expect the paths to be reported in an NSArray because we requested CFTypes. - let eventPaths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String] ?? [] + let eventPaths = unsafe unsafeBitCast(eventPaths, to: NSArray.self) as? [String] ?? [] // Compute the set of paths that were changed. let paths = eventPaths//.compactMap({ try? AbsolutePath(validating: $0) }) // <- TODO (Vlad): May be ok @@ -780,6 +783,7 @@ public protocol FSEventStreamDelegate { } /// Wrapper for Darwin's FSEventStream API. +@safe public final class FSEventStream: @unchecked Sendable { /// The errors encountered during fs event watching. @@ -814,12 +818,12 @@ public final class FSEventStream: @unchecked Sendable { self.delegate = delegate // Create the context that needs to be passed to the callback. - var callbackContext = FSEventStreamContext() - callbackContext.info = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) + var callbackContext = unsafe FSEventStreamContext() + unsafe callbackContext.info = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) // Create the stream. - self.stream = FSEventStreamCreate(nil, - callback, + unsafe self.stream = FSEventStreamCreate(nil, + callback, &callbackContext, paths as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), @@ -834,22 +838,22 @@ public final class FSEventStream: @unchecked Sendable { guard let `self` = self else { return } self.runLoop = CFRunLoopGetCurrent() // Schedule the run loop. - FSEventStreamScheduleWithRunLoop( + unsafe FSEventStreamScheduleWithRunLoop( self.stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue ) // Start the stream. - FSEventStreamScheduleWithRunLoop(self.stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) - FSEventStreamStart(self.stream) + unsafe FSEventStreamScheduleWithRunLoop(self.stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) + unsafe FSEventStreamStart(self.stream) CFRunLoopRun() // Perform cleanup. - FSEventStreamStop(self.stream) - FSEventStreamUnscheduleFromRunLoop(self.stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) - FSEventStreamInvalidate(self.stream) - FSEventStreamRelease(self.stream) + unsafe FSEventStreamStop(self.stream) + unsafe FSEventStreamUnscheduleFromRunLoop(self.stream, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) + unsafe FSEventStreamInvalidate(self.stream) + unsafe FSEventStreamRelease(self.stream) } thread.start() self.thread = thread diff --git a/Sources/AdaUtils/FixedTimestep.swift b/Sources/AdaUtils/FixedTimestep.swift index 08d727e66..f71bd3cbf 100644 --- a/Sources/AdaUtils/FixedTimestep.swift +++ b/Sources/AdaUtils/FixedTimestep.swift @@ -7,8 +7,8 @@ /// FixedTimestep enable your systems run at a fixed timestep between executions. /// This does not guarentee you that the elapsed time will be exactly fixed. -public final class FixedTimestep: @unchecked Sendable { - +public struct FixedTimestep: Sendable { + public struct AdvanceResult: Sendable { /// The elapsed time between executions. public internal(set) var fixedTime: TimeInterval @@ -34,7 +34,7 @@ public final class FixedTimestep: @unchecked Sendable { /// - Parameter deltaTime: The delta time between frame updates. /// - Returns: Advanced result with elapsed time and flag. Advance result can returns zero if that isn't fixed update. - public func advance(with deltaTime: TimeInterval) -> AdvanceResult { + public mutating func advance(with deltaTime: TimeInterval) -> AdvanceResult { var result = AdvanceResult(fixedTime: 0, isFixedTick: false) if deltaTime > 1 { diff --git a/Sources/AdaUtils/Frustum.swift b/Sources/AdaUtils/Frustum.swift index 5c3bd5fad..ece4418a7 100644 --- a/Sources/AdaUtils/Frustum.swift +++ b/Sources/AdaUtils/Frustum.swift @@ -13,7 +13,12 @@ import Math public struct Frustum: Hashable, Codable, Sendable { public var planes: FixedArray - public init(planes: FixedArray = FixedArray(repeating: Plane(normal: .zero, d: 0), count: 6)) { + public init( + planes: FixedArray = FixedArray( + repeating: Plane(normal: .zero, d: 0), + count: 6 + ) + ) { self.planes = planes } } @@ -29,7 +34,7 @@ public extension Frustum { let aabbMin = aabb.min let aabbMax = aabb.max - for plane in planes { + for unsafe plane in planes { guard let plane else { continue } diff --git a/Sources/AdaUtils/Hashes/FNVHasher.swift b/Sources/AdaUtils/Hashes/FNVHasher.swift index cc9f474e2..eeaf8d5ec 100644 --- a/Sources/AdaUtils/Hashes/FNVHasher.swift +++ b/Sources/AdaUtils/Hashes/FNVHasher.swift @@ -36,7 +36,7 @@ public mutating func combine(bytes: UnsafeRawBufferPointer) { for index in 0..( + _ valueBlock: @autoclosure () -> T?, + message: @autoclosure () -> String = String() +) -> T { + guard let value = valueBlock() else { + fatalError(message()) + } + return value +} + +public extension Optional { + @inlinable + func unwrap( + message: @autoclosure () -> String = String() + ) -> Wrapped { + require(self, message: message()) + } +} /// Call fatal error, because method not implemented public func fatalErrorMethodNotImplemented( @@ -27,6 +50,8 @@ public func TODO( ) -> Never { #if DEBUG fatalError("TODO: [\(file):\(functionName):\(line)] \(message()).") + #else + fatalError("TODO: [\(file):\(functionName):\(line)] \(message()).") #endif } diff --git a/Sources/AdaUtils/PropertyWrappers/InRange.swift b/Sources/AdaUtils/PropertyWrappers/InRange.swift index 156bc3a47..ab1359129 100644 --- a/Sources/AdaUtils/PropertyWrappers/InRange.swift +++ b/Sources/AdaUtils/PropertyWrappers/InRange.swift @@ -32,7 +32,7 @@ public struct InRange: Codable { } public init(wrappedValue: T, _ range: ClosedRange) { - self.range = Range(uncheckedBounds: (range.lowerBound, range.upperBound)) + self.range = unsafe Range(uncheckedBounds: (range.lowerBound, range.upperBound)) self.wrappedValue = Self.applyRange(self.range, for: wrappedValue) } diff --git a/Sources/AdaUtils/PropertyWrappers/MinValue.swift b/Sources/AdaUtils/PropertyWrappers/MinValue.swift index eda1c5268..6818bb158 100644 --- a/Sources/AdaUtils/PropertyWrappers/MinValue.swift +++ b/Sources/AdaUtils/PropertyWrappers/MinValue.swift @@ -34,3 +34,5 @@ public struct MinValue: Codable { return max(min, value) } } + +extension MinValue: Sendable where T: Sendable {} diff --git a/Sources/AdaUtils/RID/RID.swift b/Sources/AdaUtils/RID/RID.swift index add9c82de..a39bcf69e 100644 --- a/Sources/AdaUtils/RID/RID.swift +++ b/Sources/AdaUtils/RID/RID.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 5/21/22. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif // swiftlint:disable all @@ -18,8 +22,8 @@ public struct RID: Equatable, Hashable, Codable, Sendable { public extension RID { - nonisolated(unsafe) static var empty = RID(id: -1) - + static let empty = RID(id: -1) + /// Generate random unique rid init() { self.id = Self.readTime() @@ -27,7 +31,7 @@ public extension RID { private static func readTime() -> Int { var time = timespec(tv_sec: 0, tv_nsec: 0) - clock_gettime(CLOCK_MONOTONIC, &time) + unsafe clock_gettime(CLOCK_MONOTONIC, &time) return (time.tv_sec * 10000000) + (time.tv_nsec / 100) + 0x01B21DD213814000; } diff --git a/Sources/AdaUtils/RID/ResourceHashMap.swift b/Sources/AdaUtils/RID/ResourceHashMap.swift index 7577f71eb..b3cbf9179 100644 --- a/Sources/AdaUtils/RID/ResourceHashMap.swift +++ b/Sources/AdaUtils/RID/ResourceHashMap.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 5/21/22. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Collections /// The data type contains any values usign RID as key diff --git a/Sources/AdaUtils/Reflection/Export.swift b/Sources/AdaUtils/Reflection/Export.swift index e4e893786..4240f4500 100644 --- a/Sources/AdaUtils/Reflection/Export.swift +++ b/Sources/AdaUtils/Reflection/Export.swift @@ -15,7 +15,6 @@ /// - Note: You can use `private`, `fileprivate` modifiers, because `@Export` use reflection @propertyWrapper public struct Export: Codable, @unchecked Sendable { - private final class Storage { var skipped: Bool = false var hasChanges: Bool = false @@ -34,11 +33,11 @@ public struct Export: Codable, @unchecked Sendable { private let storage: Storage public var wrappedValue: T { - get { - self.storage.value + _read { + yield self.storage.value } - set { - self.storage.value = newValue + _modify { + yield &self.storage.value } } @@ -138,7 +137,7 @@ extension Export { extension CodingUserInfoKey { /// It will be used for editor feature. If type will be reflected with this key, we want to collect and show their properties on the editor screen. - nonisolated(unsafe) static var editorIntrospection = CodingUserInfoKey(rawValue: "export.editor.introspection")! + static let editorIntrospection = CodingUserInfoKey(rawValue: "export.editor.introspection")! } /// A protocol that defines the default value of a type. diff --git a/Sources/AdaUtils/UnsafeBox.swift b/Sources/AdaUtils/UnsafeBox.swift new file mode 100644 index 000000000..726a99db9 --- /dev/null +++ b/Sources/AdaUtils/UnsafeBox.swift @@ -0,0 +1,157 @@ +// +// UnsafeBox.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 20.11.2025. +// + +/// Holds and manage pointer to instance. +@propertyWrapper +@safe +public struct UnsafeBox { + + @usableFromInline + let box: _UnsafeBox + + @inlinable + public var wrappedValue: T { + _read { + yield unsafe box.pointer.assumingMemoryBound(to: T.self).pointee + } + _modify { + yield unsafe &box.pointer.assumingMemoryBound(to: T.self).pointee + } + } + + @inlinable + public var projectedValue: UnsafeMutablePointer { + unsafe self.box.pointer.assumingMemoryBound(to: T.self) + } + + @inlinable + public init(_ block: () -> T) { + unsafe self.box = _UnsafeBox(block()) + } + + @inlinable + public init(_ wrappedValue: consuming T) { + unsafe self.box = _UnsafeBox(wrappedValue) + } + + @inlinable + public init(_ pointer: UnsafeMutablePointer) { + unsafe self.box = _UnsafeBox(pointer) + } + + init(_ box: _UnsafeBox) { + unsafe self.box = box + } + + @inlinable + public func getPointer() -> UnsafeMutablePointer { + unsafe self.box.pointer.assumingMemoryBound(to: T.self) + } + + @inlinable + public subscript(dynamicMember dynamicMember: WritableKeyPath) -> U { + _read { + yield self.wrappedValue[keyPath: dynamicMember] + } + _modify { + yield &self.wrappedValue[keyPath: dynamicMember] + } + } +} + +extension UnsafeBox: Sendable where T: Sendable {} + +extension UnsafeBox: Equatable where T: Equatable { + public static func == (lhs: UnsafeBox, rhs: UnsafeBox) -> Bool { + lhs.wrappedValue == rhs.wrappedValue + } +} + +extension UnsafeBox: Hashable where T: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.wrappedValue) + } +} + +extension UnsafeBox: Codable where T: Codable { + public init(from decoder: any Decoder) throws { + let value = try T.init(from: decoder) + unsafe self.box = _UnsafeBox(value) + } + + public func encode(to encoder: any Encoder) throws { + try wrappedValue.encode(to: encoder) + } +} + +public extension UnsafeMutablePointer { + @inlinable + func unsafeBox() -> UnsafeBox { + unsafe UnsafeBox(self) + } +} + +@unsafe +@usableFromInline +final class _UnsafeBox: @unchecked Sendable { + @usableFromInline + let pointer: UnsafeMutableRawPointer + + private let automanaged: Bool + private let deallocator: ((UnsafeMutableRawPointer) -> Void)? + + @usableFromInline + init(_ instance: consuming T) { + unsafe pointer = .allocate(byteCount: MemoryLayout.stride, alignment: MemoryLayout.alignment) + unsafe pointer.initializeMemory(as: T.self, to: instance) + unsafe deallocator = { + unsafe $0.assumingMemoryBound(to: T.self) + .deinitialize(count: 1) + } + unsafe automanaged = true + } + + @usableFromInline + init(_ pointer: UnsafeMutablePointer) { + unsafe self.pointer = UnsafeMutableRawPointer(pointer) + unsafe self.deallocator = nil + unsafe self.automanaged = false + } + + deinit { + guard unsafe self.automanaged else { + return + } + unsafe deallocator?(pointer) + unsafe pointer.deallocate() + } +} + +@unsafe +public struct UnsafeAnyBox { + @usableFromInline + let box: _UnsafeBox + + @inlinable + public init(_ block: () -> T) { + unsafe self.box = _UnsafeBox(block()) + } + + @inlinable + public init(_ wrappedValue: consuming T) { + unsafe self.box = _UnsafeBox(wrappedValue) + } + + @inlinable + public init(_ pointer: UnsafeMutablePointer) { + unsafe self.box = _UnsafeBox(pointer) + } + + public func bind(to type: T.Type) -> UnsafeBox { + return unsafe UnsafeBox(box) + } +} diff --git a/Sources/AdaUtils/Weak/WeakSet.swift b/Sources/AdaUtils/Weak/WeakSet.swift index 0a0c5fba1..0a1413326 100644 --- a/Sources/AdaUtils/Weak/WeakSet.swift +++ b/Sources/AdaUtils/Weak/WeakSet.swift @@ -16,27 +16,28 @@ public struct WeakSet: Sequence { return self.buffer.count } + @safe public final class WeakIterator: IteratorProtocol { let buffer: [WeakBox] let currentIndex: UnsafeMutablePointer init(buffer: Set>) { self.buffer = Array(buffer.filter { !$0.isEmpty }) - self.currentIndex = UnsafeMutablePointer.allocate(capacity: 1) - self.currentIndex.pointee = -1 + unsafe self.currentIndex = UnsafeMutablePointer.allocate(capacity: 1) + unsafe self.currentIndex.pointee = -1 } deinit { - self.currentIndex.deallocate() + unsafe self.currentIndex.deallocate() } public func next() -> Element? { - self.currentIndex.pointee += 1 - if buffer.endIndex == self.currentIndex.pointee { + unsafe self.currentIndex.pointee += 1 + if unsafe buffer.endIndex == self.currentIndex.pointee { return nil } - return buffer[self.currentIndex.pointee].value + return unsafe buffer[self.currentIndex.pointee].value } } diff --git a/Sources/Math/Vectors/Vector2.swift b/Sources/Math/Vectors/Vector2.swift index e8da42f32..83d562840 100644 --- a/Sources/Math/Vectors/Vector2.swift +++ b/Sources/Math/Vectors/Vector2.swift @@ -55,6 +55,7 @@ extension Vector2: ExpressibleByArrayLiteral { } public extension Vector2 { + @inlinable subscript(_ index: Int) -> Float { get { switch index { diff --git a/Sources/Math/Vectors/Vector3.swift b/Sources/Math/Vectors/Vector3.swift index 1fd2f8e7a..cb3d9da52 100644 --- a/Sources/Math/Vectors/Vector3.swift +++ b/Sources/Math/Vectors/Vector3.swift @@ -64,6 +64,7 @@ public extension Vector3 { } public extension Vector3 { + @inlinable subscript(_ index: Int) -> Float { get { switch index { @@ -108,6 +109,7 @@ extension Vector3 { } extension Vector3: Comparable { + @inlinable public static func < (lhs: Vector3, rhs: Vector3) -> Bool { lhs.x < rhs.x && lhs.y < rhs.y && lhs.z < rhs.z } @@ -359,6 +361,7 @@ public func max(_ lhs: Vector3, _ rhs: Vector3) -> Vector3 { } /// Linearly interpolates between two points. +@inlinable public func lerp(_ lhs: Vector3, _ rhs: Vector3, _ t: Float) -> Vector3 { return lhs + (rhs - lhs) * t } diff --git a/Sources/Vulkan/Definitions/VKError.swift b/Sources/Vulkan/Definitions/VKError.swift index 41251ad07..c221d43e9 100644 --- a/Sources/Vulkan/Definitions/VKError.swift +++ b/Sources/Vulkan/Definitions/VKError.swift @@ -6,7 +6,11 @@ // import CVulkan +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif public struct VKError: LocalizedError { diff --git a/Sources/Vulkan/DescriptorSetLayout.swift b/Sources/Vulkan/DescriptorSetLayout.swift index 7803579d8..77f8ca5fd 100644 --- a/Sources/Vulkan/DescriptorSetLayout.swift +++ b/Sources/Vulkan/DescriptorSetLayout.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 10/12/21. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import CVulkan public final class DescriptorSetLayout { diff --git a/Sources/Vulkan/Device.swift b/Sources/Vulkan/Device.swift index 44f0b31f1..a4c0682dc 100644 --- a/Sources/Vulkan/Device.swift +++ b/Sources/Vulkan/Device.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 8/14/21. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import CVulkan public final class Device { diff --git a/Sources/Vulkan/ShaderModule.swift b/Sources/Vulkan/ShaderModule.swift index f202c8115..5db154324 100644 --- a/Sources/Vulkan/ShaderModule.swift +++ b/Sources/Vulkan/ShaderModule.swift @@ -6,7 +6,11 @@ // import CVulkan +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif /// A shader module. public final class ShaderModule { diff --git a/Sources/Vulkan/VulkanUtils.swift b/Sources/Vulkan/VulkanUtils.swift index 368755216..469e419a9 100644 --- a/Sources/Vulkan/VulkanUtils.swift +++ b/Sources/Vulkan/VulkanUtils.swift @@ -5,7 +5,11 @@ // Created by v.prusakov on 3/19/24. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif public enum VulkanUtils { diff --git a/Tests/AdaAssetsTests/AssetsManagerTests.swift b/Tests/AdaAssetsTests/AssetsManagerTests.swift index d94a12c18..4d1c47cb4 100644 --- a/Tests/AdaAssetsTests/AssetsManagerTests.swift +++ b/Tests/AdaAssetsTests/AssetsManagerTests.swift @@ -1,4 +1,8 @@ +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Testing @testable @_spi(AdaEngine) import AdaAssets import Math diff --git a/Tests/AdaECSTests/BUILD.bazel b/Tests/AdaECSTests/BUILD.bazel index 826d8ea36..8409976b0 100644 --- a/Tests/AdaECSTests/BUILD.bazel +++ b/Tests/AdaECSTests/BUILD.bazel @@ -3,7 +3,13 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_test") swift_test( name = "AdaECSTests", srcs = [ + "ChunkTests.swift", + "ConcurrencyTests.swift", + "Misc.swift", "QueryTests.swift", + "ShedulersTests.swift", + "SystemMacroTests.swift", + "WorldTests.swift", ], module_name = "AdaECSTests", visibility = ["//visibility:public"], diff --git a/Tests/AdaECSTests/ChunkTests.swift b/Tests/AdaECSTests/ChunkTests.swift new file mode 100644 index 000000000..b97695782 --- /dev/null +++ b/Tests/AdaECSTests/ChunkTests.swift @@ -0,0 +1,291 @@ +// +// ChunkTests.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 26.10.2025. +// + +@testable import AdaECS +import Testing +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@Component +private struct A: Equatable { + let a: Int + let b: Bool + let c: String + + init() { + self.a = .random(in: 0..<100) + self.b = .random() + self.c = UUID().uuidString + } +} + +@Component +private struct B: Equatable { + let a: String + let b: String + let c: Bool + let d: Int + + init() { + self.a = UUID().uuidString + self.b = UUID().uuidString + self.c = .random() + self.d = .random(in: 0..<100) + } +} + +@Suite("Chunks Tests") +struct ChunksTests { + var chunks: Chunks + + init() { + self.chunks = Chunks( + entitiesPerChunk: 32, + componentLayout: ComponentLayout( + componentTypes: [ + A.self, + B.self + ] + ) + ) + } + + @Test + mutating func `inserted value returns correctly`() throws { + let a = A() + let b = B() + let location = chunks.insertEntity(1, components: [a, b]) + + #expect(chunks.entities.count == 1) + #expect(chunks.chunks[location.chunkIndex].get(A.self, for: 1) == a) + #expect(chunks.chunks[location.chunkIndex].get(B.self, for: 1) == b) + } + + @Test + mutating func `moves entities works correctly`() throws { + let a = A() + let b = B() + chunks.insertEntity(1, components: [a, b]) + #expect(chunks.entities.count == 1) + + var newChunks = Chunks( + entitiesPerChunk: 32, + componentLayout: ComponentLayout(componentTypes: [A.self]) + ) + let newLocation = chunks.moveEntity(1, to: &newChunks) + #expect(chunks.entities.count == 0) + #expect(newChunks.entities.count == 1) + #expect(newChunks.chunks[newLocation.newLocation.chunkIndex].count == 1) + #expect(newChunks.chunks[newLocation.newLocation.chunkIndex].get(A.self, for: 1) == a) + #expect(newChunks.chunks[newLocation.newLocation.chunkIndex].get(B.self, for: 1) == nil) + } + + @Test + mutating func `remove entity in the first chunk`() throws { + (0..<64).forEach { index in + chunks.insertEntity(index, components: [A(), B()]) + } + + #expect(chunks.getFreeChunkIndex() == 2) + #expect(chunks.count == 3) + + (0..<32).forEach { index in + chunks.removeEntity(index) + } + + #expect(chunks.count == 3) + #expect(chunks.getFreeChunkIndex() == 0) + + (0..<32).forEach { index in + chunks.insertEntity(index, components: [A(), B()]) + } + + #expect(chunks.count == 3) + #expect(chunks.getFreeChunkIndex() == 2) + #expect(chunks.chunks[0].entities.count == 32) + #expect(chunks.chunks[1].entities.count == 32) + #expect(chunks.chunks[2].entities.count == 0) + } +} + +@Suite("Chunk Tests") +struct ChunkTests { + @Test + func `inserted entity in chunk returns correctly`() throws { + var chunk = Chunk( + entitiesPerChunk: 32, + layout: ComponentLayout( + componentTypes: [ + A.self, + B.self + ] + ) + ) + let a = A() + let b = B() + let rowId = chunk.addEntity(1)! + chunk.insert(at: rowId, components: [a, b]) + #expect(chunk.count == 1) + #expect(chunk.get(A.self, for: 1) == a) + #expect(chunk.get(B.self, for: 1) == b) + } + + @Test + func `removed entity in chunk returns correctly`() throws { + var chunk = Chunk( + entitiesPerChunk: 32, + layout: ComponentLayout( + componentTypes: [ + A.self, + B.self + ] + ) + ) + let rowId = chunk.addEntity(1)! + chunk.insert(at: rowId, components: [A(), B()]) + chunk.removeEntity(at: 1) + #expect(chunk.get(A.self, for: 1) == nil) + #expect(chunk.get(B.self, for: 1) == nil) + #expect(chunk.count == 0) + } + + @Test + func `updated components returns correctly`() throws { + var chunk = Chunk( + entitiesPerChunk: 32, + layout: ComponentLayout( + componentTypes: [ + A.self, + B.self + ] + ) + ) + let a = A() + let newA = A() + let rowId = chunk.addEntity(1)! + chunk.insert(at: rowId, components: [a, B()]) + #expect(a != newA) + #expect(chunk.get(A.self, for: 1) == a) + + chunk.getMutablePointer(A.self, for: 1)?.pointee = newA + #expect(chunk.get(A.self, for: 1) == newA) + } +} + +// MARK: Chunk Entities Manage + +extension ChunkTests { + @Test + func `filled chunk correctly`() { + let capacity = 128 + var chunk = Chunk( + entitiesPerChunk: capacity, + layout: ComponentLayout( + componentTypes: [ + A.self, + B.self + ] + ) + ) + + for index in 0..() +// query.update(from: world) +// +// // Read from multiple threads simultaneously +// await withTaskGroup(of: Int.self) { group in +// for _ in 0..<10 { +// group.addTask { +// var count = 0 +// query.wrappedValue.forEach { entity, transform, velocity in +// count += 1 +// _ = transform.position +// _ = velocity.velocity +// } +// return count +// } +// } +// +// for await count in group { +// #expect(count == 100) +// } +// } +// } +// +// @Test("Query concurrent read with modifications") +// func queryConcurrentReadWithModifications() async { +// let world = World() +// +// // Create entities +// for i in 0..<50 { +// world.spawn { +// Transform(position: [Float(i), 0, 0]) +// Velocity(velocity: [Float(i), 0, 0]) +// } +// } +// +// let query = Query, Ref>() +// query.update(from: world) +// +// // Modify from multiple threads +// await withTaskGroup(of: Void.self) { group in +// for threadId in 0..<5 { +// group.addTask { +// query.wrappedValue.forEach { entity, transform, velocity in +// transform.wrappedValue.position.y = Float(threadId) +// velocity.wrappedValue.velocity.y = Float(threadId) +// } +// } +// } +// } +// +// // Verify all entities were modified +// query.update(from: world) +// var allModified = true +// query.wrappedValue.forEach { entity, transform, velocity in +// // Position and velocity should have been modified by at least one thread +// allModified = allModified && (transform.wrappedValue.position.y >= 0) +// } +// #expect(allModified) +// } +// +// @Test("Multiple queries concurrent access") +// func multipleQueriesConcurrentAccess() async { +// let world = World() +// +// // Create entities with different component combinations +// for i in 0..<100 { +// world.spawn { +// Transform(position: [Float(i), 0, 0]) +// if i % 2 == 0 { +// Velocity(velocity: [Float(i), 0, 0]) +// } +// } +// } +// +// let query1 = Query() +// let query2 = Query() +// let query3 = Query>() +// +// query1.update(from: world) +// query2.update(from: world) +// query3.update(from: world) +// +// // Run queries concurrently +// await withTaskGroup(of: Int.self) { group in +// group.addTask { +// var count = 0 +// query1.wrappedValue.forEach { _, _ in count += 1 } +// return count +// } +// +// group.addTask { +// var count = 0 +// query2.wrappedValue.forEach { _, _, _ in count += 1 } +// return count +// } +// +// group.addTask { +// var count = 0 +// query3.wrappedValue.forEach { _, _ in count += 1 } +// return count +// } +// +// var results: [Int] = [] +// for await count in group { +// results.append(count) +// } +// +// #expect(results.contains(100)) // query1 and query3 +// #expect(results.contains(50)) // query2 +// } +// } +// +// // MARK: - World Concurrency Tests +// +// @Test("World concurrent entity spawning") +// func worldConcurrentEntitySpawning() async { +// let world = World() +// // Spawn entities from multiple threads +// let commands = await withTaskGroup(of: Commands.self) { group in +// for threadId in 0..<10 { +// group.addTask { +// // Create separate Commands for each task to avoid race conditions +// let taskCommands = world.makeCommands() +// for index in 0..<10 { +// taskCommands.spawn { +// Transform(position: [Float(threadId * 10 + index), 0, 0]) +// Velocity(velocity: [Float(threadId), 0, 0]) +// } +// } +// return taskCommands +// } +// } +// +// // Collect and merge all commands +// let mainCommands = world.makeCommands() +// for await taskCommands in group { +// mainCommands.append(taskCommands) +// } +// return mainCommands +// } +// +// commands.finish(world) +// world.flush() +// +// let query = Query() +// query.update(from: world) +// +// #expect(query.wrappedValue.count == 100) +// } +// +// @Test("World concurrent component insertion") +// func worldConcurrentComponentInsertion() async { +// let world = World() +// +// // Create entities first +// var entities: [Entity] = [] +// for i in 0..<50 { +// let entity = world.spawn { +// Transform(position: [Float(i), 0, 0]) +// } +// entities.append(entity) +// } +// +// world.flush() +// +// // Insert Velocity component from multiple threads +// let entityIds = entities.map { $0.id } +// await withTaskGroup(of: Void.self) { group in +// for i in 0..<5 { +// group.addTask { [entityIds] in +// let start = i * 10 +// let end = start + 10 +// for idx in start..() +// query.update(from: world) +// +// #expect(query.wrappedValue.count == 50) +// } +// +//// @Test("World concurrent component removal") +//// func worldConcurrentComponentRemoval() async { +//// let world = World() +//// +//// // Create entities with components +//// var entities: [Entity] = [] +//// for i in 0..<50 { +//// let entity = world.spawn { +//// Transform(position: [Float(i), 0, 0]) +//// Velocity(velocity: [Float(i), 0, 0]) +//// } +//// entities.append(entity) +//// } +//// +//// world.flush() +//// +//// // Remove Velocity component from multiple threads +//// let entityIds = entities.map { $0.id } +//// await withTaskGroup(of: Void.self) { group in +//// for i in 0..<5 { +//// group.addTask { [entityIds] in +//// let start = i * 10 +//// let end = start + 10 +//// for idx in start..() +//// query.update(from: world) +//// +//// // All velocity components should be removed +//// #expect(query.wrappedValue.count == 0) +//// +//// // But Transform should still exist +//// let transformQuery = Query() +//// transformQuery.update(from: world) +//// #expect(transformQuery.wrappedValue.count == 50) +//// } +//// +// @Test("World concurrent entity removal") +// func worldConcurrentEntityRemoval() async { +// let world = World() +// +// // Create entities +// var entities: [Entity] = [] +// for i in 0..<100 { +// let entity = world.spawn { +// Transform(position: [Float(i), 0, 0]) +// } +// entities.append(entity) +// } +// +// world.flush() +// +// // Remove entities from multiple threads +// let entityCopies = entities +// await withTaskGroup(of: Void.self) { group in +// for i in 0..<10 { +// group.addTask { [entityCopies] in +// let start = i * 10 +// let end = start + 10 +// for idx in start..() +// query.update(from: world) +// +// #expect(query.wrappedValue.count == 0) +// } +// +// @Test("World concurrent read and write") +// func worldConcurrentReadAndWrite() async { +// let world = World() +// +// // Create entities +// for i in 0..<50 { +// world.spawn { +// Transform(position: [Float(i), 0, 0]) +// Velocity(velocity: [Float(i), 0, 0]) +// } +// } +// +// world.flush() +// +// let query = Query, Ref>() +// query.update(from: world) +// +// // Concurrently read and write to components +// await withTaskGroup(of: Void.self) { group in +// // Writer threads +// for threadId in 0..<3 { +// group.addTask { +// query.wrappedValue.forEach { entity, transform, velocity in +// transform.wrappedValue.position.y = Float(threadId) +// velocity.wrappedValue.velocity.y = Float(threadId) +// } +// } +// } +// +// // Reader threads +// for _ in 0..<3 { +// group.addTask { +// query.wrappedValue.forEach { entity, transform, velocity in +// _ = transform.wrappedValue.position +// _ = velocity.wrappedValue.velocity +// } +// } +// } +// } +// +// // Verify data integrity +// query.update(from: world) +// var validData = true +// query.wrappedValue.forEach { entity, transform, velocity in +// // Position should be a valid value (0, 1, or 2) +// validData = validData && (transform.wrappedValue.position.y >= 0 && transform.wrappedValue.position.y <= 2) +// } +// #expect(validData) +// } +// +// // MARK: - Resource Concurrency Tests +// +// @Test("Resource concurrent read access") +// func resourceConcurrentReadAccess() async { +// let world = World() +// world.insertResource(Gravity(value: Vector3(0, -9.8, 0))) +// +// // Read resource from multiple threads +// await withTaskGroup(of: Vector3.self) { group in +// for _ in 0..<10 { +// group.addTask { +// guard let gravity = world.getResource(Gravity.self) else { +// return .zero +// } +// return gravity.value +// } +// } +// +// for await value in group { +// #expect(value == Vector3(0, -9.8, 0)) +// } +// } +// } +// +// @Test("Resource concurrent write access") +// func resourceConcurrentWriteAccess() async { +// let world = World() +// world.insertResource(Gravity(value: Vector3(0, -9.8, 0))) +// +// // Write resource from multiple threads +// await withTaskGroup(of: Void.self) { group in +// for i in 0..<10 { +// group.addTask { +// let mutable = world.getRefResource(Gravity.self) +// mutable.wrappedValue.value.y = Float(-i) +// } +// } +// } +// +// // Resource should have been modified +// let gravity = world.getResource(Gravity.self) +// #expect(gravity != nil) +// #expect(gravity!.value.y != -9.8) +// } +// +// // MARK: - Stress Tests +// +// @Test("Stress test: Heavy concurrent operations") +// func stressTestHeavyConcurrentOperations() async { +// let world = World() +// +// // Create initial entities +// for i in 0..<200 { +// world.spawn { +// Transform(position: [Float(i), 0, 0]) +// if i % 2 == 0 { +// Velocity(velocity: [Float(i), 0, 0]) +// } +// } +// } +// +// world.flush() +// +// // Perform heavy concurrent operations +// await withTaskGroup(of: Void.self) { group in +// // Spawn new entities +// for _ in 0..<3 { +// group.addTask { +// for i in 0..<20 { +// world.spawn { +// Transform(position: [Float(i), 0, 0]) +// Velocity(velocity: [Float(i), 0, 0]) +// } +// } +// } +// } +// +// // Query and modify +// for _ in 0..<3 { +// group.addTask { +// let query = Query>() +// query.update(from: world) +// query.wrappedValue.forEach { entity, transform in +// transform.wrappedValue.position.y += 1.0 +// } +// } +// } +// +// // Insert components +// for _ in 0..<2 { +// group.addTask { +// let query = Query() +// query.update(from: world) +// var count = 0 +// query.wrappedValue.forEach { entity, _ in +// if count % 5 == 0 { +// world.insert(Velocity(velocity: [1, 2, 3]), for: entity.id) +// } +// count += 1 +// } +// } +// } +// } +// +// world.flush() +// +// // Verify world is still consistent +// let allEntities = world.getEntities() +// #expect(allEntities.count > 200) +// } +// +// @Test("Stress test: Query iteration race condition") +// func stressTestQueryIterationRaceCondition() async { +// let world = World() +// +// // Create many entities +// for i in 0..<1000 { +// world.spawn { +// Transform(position: [Float(i), 0, 0]) +// Velocity(velocity: [Float(i % 100), 0, 0]) +// } +// } +// +// world.flush() +// +// let query = Query, Ref>() +// query.update(from: world) +// +// // Iterate and modify concurrently +// await withTaskGroup(of: Int.self) { group in +// for threadId in 0..<20 { +// group.addTask { +// var count = 0 +// query.wrappedValue.forEach { entity, transform, velocity in +// // Read and write +// let oldPos = transform.wrappedValue.position +// transform.wrappedValue.position.x = oldPos.x + Float(threadId) +// velocity.wrappedValue.velocity.y = Float(threadId) +// count += 1 +// } +// return count +// } +// } +// +// for await count in group { +// // Each thread should iterate over all 1000 entities +// #expect(count == 1000) +// } +// } +// } +//} +// +// diff --git a/Tests/AdaECSTests/Misc.swift b/Tests/AdaECSTests/Misc.swift new file mode 100644 index 000000000..64b5fe17d --- /dev/null +++ b/Tests/AdaECSTests/Misc.swift @@ -0,0 +1,17 @@ +import Testing +@_spi(Internal) @testable import AdaECS +import Math + +@Component +struct Transform: Hashable { + var position: Vector3 = .zero +} + +@Component +struct Velocity: Hashable { + var velocity: Vector3 = .zero +} + +struct Gravity: Resource, Hashable { + var value: Vector3 +} \ No newline at end of file diff --git a/Tests/AdaECSTests/QueryTests.swift b/Tests/AdaECSTests/QueryTests.swift index 23789b3e1..c80223a52 100644 --- a/Tests/AdaECSTests/QueryTests.swift +++ b/Tests/AdaECSTests/QueryTests.swift @@ -2,16 +2,6 @@ import Testing @_spi(Internal) @testable import AdaECS import Math -@Component -struct Transform { - var position: Vector3 = .zero -} - -@Component -struct Velocity { - var velocity: Vector3 = .zero -} - @Suite("Query tests") struct QueryTests { @Test("Query fetch components") @@ -19,15 +9,11 @@ struct QueryTests { let world = World() for _ in 0..<10 { - let entity = Entity() - entity.components += Transform() - entity.components += Velocity() - - world.addEntity(entity) + world.spawn { + Transform() + Velocity() + } } - - world.build() - let query = Query() query.update(from: world) @@ -43,13 +29,11 @@ struct QueryTests { let world = World() for i in 0..<5 { - let entity = Entity() - entity.components += Transform(position: [Float(i), 0, 0]) - world.addEntity(entity) + world.spawn { + Transform(position: [Float(i), 0, 0]) + } } - world.build() - let query = Query>() query.update(from: world) @@ -71,18 +55,14 @@ struct QueryTests { // Add entities with different component combinations for i in 0..<10 { - let entity = Entity() - entity.components += Transform() - - if i % 2 == 0 { - entity.components += Velocity() + world.spawn { + Transform() + if i % 2 == 0 { + Velocity() + } } - - world.addEntity(entity) } - world.build() - // Query only entities with both Transform and Velocity let query = Query() query.update(from: world) @@ -97,20 +77,19 @@ struct QueryTests { let query = Query() query.update(from: world) #expect(query.wrappedValue.count == 0) - + // Add entity - let entity = Entity() - entity.components += Transform() - world.addEntity(entity) - world.build() - + let entity = world.spawn { + Transform() + } + query.update(from: world) #expect(query.wrappedValue.count == 1) // Remove component entity.components.remove(Transform.self) - await world.update(1.0 / 60.0) - + await world.runScheduler(.update) + query.update(from: world) #expect(query.wrappedValue.count == 0) } @@ -119,106 +98,93 @@ struct QueryTests { func queryWithOptionalComponents() async { let world = World() - let entity = Entity() - entity.components += Transform() - world.addEntity(entity) + let entity = world.spawn { + Transform() + } let query = Query, Velocity>() query.update(from: world) #expect(query.wrappedValue.count == 0) - world.build() - entity.components += Velocity() - - await world.update(1.0 / 60.0) - query.update(from: world) #expect(query.wrappedValue.count == 1) } @Test("FilterQuery with With filter") - func filterQueryWithWithFilter() { + func filterQueryWithWithFilter() async { let world = World() for i in 0..<5 { - let entity = Entity() - if i % 2 == 0 { - entity.components += Transform() + world.spawn { + if i % 2 == 0 { + Transform() + } } - world.addEntity(entity) } - world.build() - let query = FilterQuery>() - query.update(from: world) - #expect(query.wrappedValue.count == 3) + let query = world.performQuery(FilterQuery>()) + #expect(query.count == 3) } @Test("FilterQuery with WithOut filter") func filterQueryWithWithOutFilter() { let world = World() for i in 0..<5 { - let entity = Entity() - if i % 2 == 0 { - entity.components += Transform() + world.spawn { + if i % 2 == 0 { + Transform() + } } - world.addEntity(entity) } - world.build() - let query = FilterQuery>() - query.update(from: world) - #expect(query.wrappedValue.count == 2) + let query = world.performQuery(FilterQuery>()) + #expect(query.count == 2) } @Test("FilterQuery with And filter") - func filterQueryWithAndFilter() { + func filterQueryWithAndFilter() async { let world = World() for i in 0..<6 { - let entity = Entity() - if i % 2 == 0 { - entity.components += Transform() + world.spawn { + if i % 2 == 0 { + Transform() + } + if i % 3 == 0 { + Velocity() + } } - if i % 3 == 0 { - entity.components += Velocity() - } - world.addEntity(entity) } - world.build() - let query = FilterQuery, With>>() - query.update(from: world) + await world.runScheduler(.update) + let query = world.performQuery(FilterQuery, With>>()) // Only entities with both Transform and Velocity (i == 0) - #expect(query.wrappedValue.count == 1) + #expect(query.count == 1) } @Test("FilterQuery with Or filter") func filterQueryWithOrFilter() { let world = World() for i in 0..<6 { - let entity = Entity() - if i % 2 == 0 { - entity.components += Transform() - } - if i % 3 == 0 { - entity.components += Velocity() + world.spawn { + if i % 2 == 0 { + Transform() + } + if i % 3 == 0 { + Velocity() + } } - world.addEntity(entity) } - world.build() - let query = FilterQuery, With>>() - query.update(from: world) + let query = world.performQuery(FilterQuery, With>>()) // Entities with either Transform or Velocity (i == 0,1,2,3,4,5 except i==1,4) - #expect(query.wrappedValue.count == 4) + #expect(query.count == 4) } @Test("FilterQuery with NoFilter") func filterQueryWithNoFilter() { let world = World() for _ in 0..<4 { - let entity = Entity() - entity.components += Transform() - world.addEntity(entity) + world.spawn { + Transform() + } } - world.build() - let query = FilterQuery() + let query = Query() query.update(from: world) #expect(query.wrappedValue.count == 4) } -} \ No newline at end of file +} diff --git a/Tests/AdaECSTests/ShedulersTests.swift b/Tests/AdaECSTests/ShedulersTests.swift new file mode 100644 index 000000000..cd0a55962 --- /dev/null +++ b/Tests/AdaECSTests/ShedulersTests.swift @@ -0,0 +1,52 @@ +// +// ShedulersTests.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 06.11.2025. +// + +import AdaECS +import Testing + +@Suite +struct SchedulersTests { + @Test + func `add system to existing schedule`() async throws { + let world = World() + world.addScheduler(.init(name: .test)) + world.insertResource(CheckSystemMarker(value: 0)) + world.addSystem(checkSystem.self, on: .test) + + await world.runScheduler(.test) + + let marker = try #require(world.getResource(CheckSystemMarker.self)) + #expect(marker.value == 1, "CheckSystem should exists for Tests scheduler") + } + + @Test + func `add system to non existing schedule`() async throws { + let world = World() + + world.insertResource(CheckSystemMarker(value: 0)) + world.addSystem(checkSystem.self, on: .test) + + await world.runScheduler(.test) + + let marker = try #require(world.getResource(CheckSystemMarker.self)) + #expect(marker.value == 1, "CheckSystem should exists for Tests scheduler") + } +} + +struct CheckSystemMarker: Resource { + var value: Int +} + +@System +func check(_ res: ResMut) { + res.value += 1 + print(res.wrappedValue) +} + +private extension SchedulerName { + static let test: SchedulerName = "Tests" +} diff --git a/Tests/AdaECSTests/SystemMacroTests.swift b/Tests/AdaECSTests/SystemMacroTests.swift new file mode 100644 index 000000000..eee50b090 --- /dev/null +++ b/Tests/AdaECSTests/SystemMacroTests.swift @@ -0,0 +1,138 @@ +// +// SystemMacroTests.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 05.06.2025. +// + +import Testing +@testable import AdaECS +import Math + +@Suite +struct SystemMacroTests { + + @Test + func testBasicSystemMacro() throws { + let world = World() + let system = BasicSystem(world: world) + #expect(system.queries.queries.count == 1) + } + + @Test + func testSystemWithDependencies() throws { + #expect(DependentSystem.dependencies.count == 1) + #expect(DependentSystem.dependencies[0] == .after(PhysicsSystem.self)) + } + + @Test + func testSystemWithResources() throws { + let world = World() + world.insertResource(Gravity(value: Vector3(0, -9.8, 0))) + let system = ResourceSystem(world: world) + #expect(system.queries.queries.count == 1) + } + + @Test + func testPlainSystemWithResources() throws { + let world = World() + world.insertResource(Gravity(value: Vector3(0, -9.8, 0))) + let system = PlainResourceSystem(world: world) + #expect(system.queries.queries.count == 1) + } + + @Test + func testSystemWithUpdateContext() throws { + let world = World(name: "PlainContext") + let system = PlainContextSystem(world: world) + + #expect(system.queries.queries.count == 0) + } + + @Test + func testPlainSystemMacro() throws { + let world = World(name: "PlainWorld") + let system = PlainTransformSystem(world: world) + + #expect(system.queries.queries.count == 1) + } +} + +@PlainSystem +struct BasicSystem { + @Query, Velocity> + private var query + + init(world: World) {} + + func update(context: UpdateContext) { + query.forEach { (transform, velocity) in + transform.position += velocity.velocity + } + } +} + +@System +func PlainTransform( + _ query: Query> +) { + for transform in query.wrappedValue { + transform.position += Vector3(1, 0, 0) + } +} + +@System +func PlainContext( + _ context: WorldUpdateContext +) { + #expect(context.world.name == "PlainContext") +} + +@System +func PlainWorld( + _ world: World +) { + #expect(world.name == "PlainWorld") +} + +@PlainSystem +struct PhysicsSystem { + init(world: World) { } + + func update(context: UpdateContext) {} +} + +@PlainSystem(dependencies: [ + .after(PhysicsSystem.self) +]) +struct DependentSystem: System { + @Query> + private var query + + init(world: World) {} + + func update(context: UpdateContext) { + for transform in query { + transform.position += Vector3(1, 0, 0) + } + } +} + +@PlainSystem +struct ResourceSystem { + @Res + private var gravity: Gravity? + + init(world: World) { } + + func update(context: UpdateContext) { + #expect(gravity != nil) + } +} + +@System +func PlainResource( + _ gravity: Res +) { + #expect(gravity.wrappedValue != nil) +} diff --git a/Tests/AdaECSTests/WorldTests.swift b/Tests/AdaECSTests/WorldTests.swift new file mode 100644 index 000000000..25ec5ad80 --- /dev/null +++ b/Tests/AdaECSTests/WorldTests.swift @@ -0,0 +1,432 @@ +import Testing +@_spi(Internal) @testable import AdaECS +import Math + +@Component +struct ComponentA: Equatable { + var value: Int +} + +@Component +struct ComponentB: Equatable { + var value: String +} + +@Component +struct ComponentC: Equatable { } + +struct TestResource: Resource, Equatable { + var value: Int +} + +@Suite("World Tests") +struct WorldTests { + let world: World + + init() { + self.world = World() + } + + @Test("Get resource") + func getResource() { + let resource = Gravity(value: Vector3(0, -9.8, 0)) + world.insertResource(resource) + let resource2 = world.getResource(Gravity.self) + #expect(resource == resource2) + } + + @Test("Get mutable resource") + func getMutableResource() throws { + let resource = Gravity(value: Vector3(0, 9.8, 0)) + world.insertResource(resource) + world.getRefResource(Gravity.self).wrappedValue.value = Vector3(0, -9.8, 0) + #expect(world.getRefResource(Gravity.self).wrappedValue.value == Vector3(0, -9.8, 0)) + } + + @Test("Spawn empty entity") + func spawnEmptyEntity() { + let entity = world.spawn() + #expect(entity.components.isEmpty) + } + + @Test("Spawn entity with component") + func spawnEntityWithComponent() { + let transform = Transform(position: Vector3(0, 9.8, 0)) + let entity = world.spawn { + transform + } + #expect(entity.components.count == 1) + #expect(entity.components[Transform.self] == transform) + } + + @Test("Remove entity") + func removeEntity() { + let entity = world.spawn() + world.removeEntity(entity) + #expect(world.entities.entities.count == 0) + } + + @Test("Command spawn") + func commandSpawn() { + let transform = Transform(position: Vector3(0, 9.8, 0)) + let commands = world.makeCommands() + commands.spawn { + transform + } + commands.finish(world) + world.flush() + #expect(world.entities.entities.count == 1) + } + + @Test("Clear world") + func clearAllWorld() { + let entityCount = 1000 + for i in 0..() + query.update(from: world) + + #expect(query.count == entityCount) + + world.clear() + + #expect(world.entities.entities.isEmpty) + #expect(world.addedEntities.isEmpty) + #expect(world.removedEntities.isEmpty) + + query.update(from: world) + #expect(query.count == 0) + } + + @Test + func requiredComponentsInitialized() throws { + world.registerRequiredComponent(ComponentA.self, ComponentB.self) { + ComponentB(value: "test1") + } + + let ent = world.spawn { + ComponentA(value: 1) + } + let componentA = try #require(ent.components[ComponentA.self]) + let componentB = try #require(ent.components[ComponentB.self]) + #expect(componentA.value == 1) + #expect(componentB.value == "test1") + } +} + +extension WorldTests { + @Test("Random Access") + func randomAccess() { + let e = world.spawn { + ComponentA(value: 123) + ComponentB(value: "abc") + } + + let f = world.spawn { + ComponentA(value: 456) + ComponentB(value: "def") + ComponentC() + } + + #expect(world.get(from: e.id) == ComponentA(value: 123)) + #expect(world.get(from: e.id) == ComponentB(value: "abc")) + #expect(world.get(from: f.id) == ComponentA(value: 456)) + #expect(world.get(from: f.id) == ComponentB(value: "def")) + #expect(world.get(from: f.id) == ComponentC()) + + world.insert(ComponentB(value: "xyz"), for: e.id) + #expect(world.get(from: e.id) == ComponentB(value: "xyz")) + } + + @Test("Chunk Creation") + func chunkCreation() { + let entityCount = 100 + for i in 0..() + query.update(from: world) + + #expect(query.count == entityCount) + } + + @Test("Despawn") + func despawn() { + let e = world.spawn { + ComponentA(value: 123) + ComponentB(value: "abc") + } + + let f = world.spawn { + ComponentA(value: 456) + ComponentB(value: "def") + } + + #expect(world.getEntityByID(f.id) != nil) + #expect(world.getEntityByID(e.id) != nil) + + world.removeEntity(e) + + #expect(world.getEntityByID(e.id) == nil) + #expect(world.get(from: e.id) as ComponentA? == nil) + #expect(world.get(from: e.id) as ComponentB? == nil) + + #expect(world.getEntityByID(f.id) != nil) + #expect(world.get(from: f.id) == ComponentA(value: 456)) + #expect(world.get(from: f.id) == ComponentB(value: "def")) + } + + @Test("Query all") + func queryAll() { + let e = world.spawn { + ComponentB(value: "abc") + ComponentA(value: 123) + } + + let f = world.spawn { + ComponentB(value: "def") + ComponentA(value: 456) + } + + let query = Query() + query.update(from: world) + + var results: [(Entity, ComponentA, ComponentB)] = [] + query.forEach { item in + results.append(item) + } + + #expect(results.count == 2) + + // The order is not guaranteed, so we need to check for existence of both. + let expected: [(Entity, ComponentA, ComponentB)] = [ + (e, ComponentA(value: 123), ComponentB(value: "abc")), + (f, ComponentA(value: 456), ComponentB(value: "def")) + ] + + for expectedItem in expected { + #expect(results.contains(where: { $0.0.id == expectedItem.0.id && $0.1 == expectedItem.1 && $0.2 == expectedItem.2 })) + } + } + + @Test("Query filter with") + func queryFilterWith() { + world.spawn { + ComponentA(value: 123) + ComponentB(value: "a") + } + world.spawn { + ComponentA(value: 456) + } + + let query = world.performQuery(FilterQuery>()) + let results = query.map { $0 } + + #expect(results.count == 1) + #expect(results.first == ComponentA(value: 123)) + } + + @Test("Query filter without") + func queryFilterWithout() { + world.spawn { + ComponentA(value: 123) + ComponentB(value: "a") + } + world.spawn { + ComponentA(value: 456) + } + + let query = world.performQuery(FilterQuery>()) + let results = query.map { $0 } + + #expect(results.count == 1) + #expect(results.first == ComponentA(value: 456)) + } + + @Test("Add and remove components") + func addRemoveComponents() { + let e = world.spawn { + ComponentA(value: 1) + ComponentB(value: "a") + } + + // Check initial state + #expect(world.has(ComponentA.self, in: e.id)) + #expect(world.has(ComponentB.self, in: e.id)) + #expect(!world.has(ComponentC.self, in: e.id)) + + // Add ComponentC + world.insert(ComponentC(), for: e.id) + #expect(world.has(ComponentC.self, in: e.id)) + + // Remove ComponentA + world.remove(ComponentA.self, from: e.id) + #expect(!world.has(ComponentA.self, in: e.id)) + #expect(world.get(from: e.id) as ComponentA? == nil) + #expect(world.has(ComponentB.self, in: e.id)) + #expect(world.has(ComponentC.self, in: e.id)) + } + + @Test("Query optional component") + func queryOptionalComponent() { + let e = world.spawn { + ComponentA(value: 123) + } + + let f = world.spawn { + ComponentA(value: 456) + ComponentB(value: "b") + } + + let query = FilterQuery, NoFilter>() + query.update(from: world) + + var results: [(Entity.ID, ComponentA, Optional)] = [] + query.forEach { entity, a, b in + results.append((entity.id, a, b)) + } + + #expect(results.count == 2) + + let resultE = results.first(where: { $0.0 == e.id }) + #expect(resultE?.1 == ComponentA(value: 123)) + #expect(resultE?.2 == nil) + + let resultF = results.first(where: { $0.0 == f.id }) + #expect(resultF?.1 == ComponentA(value: 456)) + #expect(resultF?.2 == ComponentB(value: "b")) + } + + @Test("Spawn Batch") + func spawnBatch() { + let entityCount = 1000 + for i in 0..() + query.update(from: world) + + #expect(query.count == entityCount) + + var sum = 0 + query.forEach { a in + sum += a.value + } + + let expectedSum = (0..>() + query.update(from: world) + + for (index, item) in query.enumerated() { + if index % 2 == 0 { + item.1.wrappedValue.value += 1 + #expect(item.1.isChanged) + } + } + + let changedQuery = world.performQuery(FilterQuery>()) + let changedEntities = Set(changedQuery.map { $0.id }) + + #expect(changedEntities.count == 1) + #expect(changedEntities.contains(e1.id)) + #expect(!changedEntities.contains(e2.id)) + + // Ensure moving archetypes preserves change state + world.insert(ComponentB(value: "moved"), for: e1.id) + + let changedQueryAfterMove = world.performQuery(FilterQuery>()) + let changedEntitiesAfterMove = Set(changedQueryAfterMove.map { $0.id }) + + #expect(changedEntitiesAfterMove.count == 1) + #expect(changedEntitiesAfterMove.contains(e1.id)) + } + + @Test + func requiredComponent() { + world.registerRequiredComponent(ComponentA.self, RequiredComponentForA.self) { + RequiredComponentForA(someValue: "some value") + } + + let entity = world.spawn("Some entity") { + ComponentB(value: "value") + } + + #expect(entity.components[ComponentB.self]?.value == "value") + #expect(entity.components[ComponentA.self] == nil) + #expect(entity.components[RequiredComponentForA.self] == nil) + + entity.components += ComponentA(value: 1) + + #expect(entity.components[ComponentA.self]?.value == 1) + #expect(entity.components[RequiredComponentForA.self]?.someValue == "some value") + } +} + +@Component +struct RequiredComponentForA { + let someValue: String +} diff --git a/Tests/AdaEngineTests/FixedTimestepTests.swift b/Tests/AdaEngineTests/FixedTimestepTests.swift index 7834e5b18..bc91a2c59 100644 --- a/Tests/AdaEngineTests/FixedTimestepTests.swift +++ b/Tests/AdaEngineTests/FixedTimestepTests.swift @@ -20,7 +20,7 @@ struct FixedTimestepTests { @Test func accumulation() { let stepsPerSecond: Float = 1 / 60 - let timestep = FixedTimestep(stepsPerSecond: 60) + var timestep = FixedTimestep(stepsPerSecond: 60) let result = timestep.advance(with: stepsPerSecond / 2) #expect(result.isFixedTick == false) @@ -32,7 +32,7 @@ struct FixedTimestepTests { @Test func multipleTicks() { let stepsPerSecond: Float = 1.0 / 60.0 - let timestep = FixedTimestep(stepsPerSecond: 60) + var timestep = FixedTimestep(stepsPerSecond: 60) let result = timestep.advance(with: stepsPerSecond * 3) #expect(result.isFixedTick) @@ -42,7 +42,7 @@ struct FixedTimestepTests { @Test func maxAccumulation() { let stepsPerSecond: Float = 1.0 / 60.0 - let timestep = FixedTimestep(stepsPerSecond: 60) + var timestep = FixedTimestep(stepsPerSecond: 60) let result = timestep.advance(with: TimeInterval(stepsPerSecond * 20)) diff --git a/Tests/AdaEngineTests/Physics2DTests.swift b/Tests/AdaEngineTests/Physics2DTests.swift index 9177bc325..90ebb7ccb 100644 --- a/Tests/AdaEngineTests/Physics2DTests.swift +++ b/Tests/AdaEngineTests/Physics2DTests.swift @@ -17,95 +17,81 @@ struct Physics2DTests { let world: AppWorlds init() async throws { - let world = AppWorlds(mainWorld: World()) + let world = AppWorlds(main: World()) self.world = world - world.insertResource(DefaultSchedulerOrder(order: [.update, .fixedUpdate, .postUpdate])) - world.mainWorld.setSchedulers([ - .update, - .fixedUpdate, - .postUpdate - ]) world + .addPlugin(MainSchedulerPlugin()) .addPlugin(Physics2DPlugin()) .addPlugin(TransformPlugin()) - try world.build() - } - - @Test - func createStaticBody() async throws { - let entity = Entity() - - let collision = Collision2DComponent( - shapes: [.generateBox()], - mode: .default - ) - - entity.components += collision - entity.components += Transform(position: [0, -10, 0]) - - world.addEntity(entity) - world.mainWorld.flush() - await world.mainWorld.runScheduler(.fixedUpdate, deltaTime: 1 / 60) - - let runtimeBody = try #require(entity.components[Collision2DComponent.self]?.runtimeBody) - #expect(runtimeBody.getPosition() == [0, -10]) - } - - @Test - func dynamicBodyFalling() async { - let ground = Entity() - let groundShape = Shape2DResource.generateBox(width: 100, height: 10) - let groundCollision = Collision2DComponent(shapes: [groundShape], mode: .default) - ground.components.set(groundCollision) - ground.components += Transform(position: [0, -10, 0]) - world.addEntity(ground) - - let box = Entity() - let boxShape = Shape2DResource.generateBox(width: 1, height: 1) - let boxCollision = PhysicsBody2DComponent( - shapes: [boxShape], - mass: 1, - mode: .dynamic - ) - box.components += boxCollision - box.components += Transform(position: [0, 10, 0]) - world.addEntity(box) - - let startY = box.components[Transform.self]?.position.y ?? 0 - - for _ in 0..<60 { - await world.update() - } - - let endY = box.components[Transform.self]?.position.y ?? 0 - #expect(endY < startY) - #expect(endY > -9) - } - - @Test - func applyForce() async { - let box = Entity() - let physicsBody = PhysicsBody2DComponent( - shapes: [.generateBox()], - mass: 1, - mode: .dynamic - ) - box.components += physicsBody - box.components += Transform(position: .zero) - world.addEntity(box) - world.mainWorld.flush() - await world.update() - - box.components[PhysicsBody2DComponent.self]?.applyForceToCenter([100, 0], wake: true) - - let initialVelocity = box.components[PhysicsBody2DComponent.self]!.linearVelocity.x - - await world.update() - - let finalVelocity = box.components[PhysicsBody2DComponent.self]!.linearVelocity.x - #expect(finalVelocity > initialVelocity) + try await world.build() } +// +// @Test +// func createStaticBody() async throws { +// let entity = world.main.spawn { +// Collision2DComponent( +// shapes: [.generateBox()], +// mode: .default +// ) +// Transform(position: [0, -10, 0]) +// } +// +// await world.update() +// +// let runtimeBody = try #require(entity.components[Collision2DComponent.self]?.runtimeBody) +// #expect(runtimeBody.getPosition() == [0, -10]) +// } +// +// @Test +// func dynamicBodyFalling() async { +// world.main.spawn { +// Collision2DComponent( +// shapes: [Shape2DResource.generateBox(width: 100, height: 10)], +// mode: .default +// ) +// Transform(position: [0, -10, 0]) +// } +// +// let box = world.main.spawn { +// PhysicsBody2DComponent( +// shapes: [Shape2DResource.generateBox(width: 1, height: 1)], +// mass: 1, +// mode: .dynamic +// ) +// Transform(position: [0, 10, 0]) +// } +// +// let startY = box.components[Transform.self]?.position.y ?? 0 +// +// for _ in 0..<60 { +// await world.update() +// } +// +// let endY = box.components[Transform.self]?.position.y ?? 0 +// #expect(endY < startY) +// #expect(endY > -9) +// } +// +// @Test +// func applyForce() async { +// let box = world.main.spawn { +// PhysicsBody2DComponent( +// shapes: [.generateBox()], +// mass: 1, +// mode: .dynamic +// ) +// Transform(position: .zero) +// } +// await world.update() +// box.components[PhysicsBody2DComponent.self]?.applyForceToCenter([100, 0], wake: true) +// let initialVelocity = box.components[PhysicsBody2DComponent.self]!.linearVelocity.x +// +// await world.update() +// +// let finalVelocity = box.components[PhysicsBody2DComponent.self]!.linearVelocity.x +// #expect(finalVelocity > initialVelocity) +// } // @Test // @MainActor diff --git a/Tests/AdaInputTests/GamepadInputTests.swift b/Tests/AdaInputTests/GamepadInputTests.swift index d66f8eac4..ea6bd628b 100644 --- a/Tests/AdaInputTests/GamepadInputTests.swift +++ b/Tests/AdaInputTests/GamepadInputTests.swift @@ -9,13 +9,16 @@ import Testing @testable import AdaUI @_spi(Internal) @testable import AdaInput import AdaUtils +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif import Math @Suite("Gamepad Input Tests") -final class GamepadInputTests: Sendable { - - let input: Input +struct GamepadInputTests: Sendable { + var input: Input init() async { // Clear gamepad states and events before each test @@ -25,7 +28,7 @@ final class GamepadInputTests: Sendable { } @Test("Gamepad Connection and Disconnection") - func testGamepadConnectionAndDisconnection() async { + mutating func testGamepadConnectionAndDisconnection() async { #expect(self.input.getConnectedGamepads().count == 0, "Gamepads should be empty.") let gamepadId = 0 @@ -61,7 +64,7 @@ final class GamepadInputTests: Sendable { } @Test("Gamepad Button Press and Release") - func testGamepadButtonPressAndRelease() async throws { + mutating func testGamepadButtonPressAndRelease() async throws { #expect(self.input.getConnectedGamepads().count == 0, "Gamepads should be empty.") let gamepadId = 1 @@ -106,7 +109,7 @@ final class GamepadInputTests: Sendable { } @Test("Gamepad Axis Value") - func testGamepadAxisValue() async { + mutating func testGamepadAxisValue() async { let gamepadId = 2 let windowId = UIWindow.ID.empty var time = AdaUtils.TimeInterval(Date().timeIntervalSince1970) @@ -138,7 +141,7 @@ final class GamepadInputTests: Sendable { } @Test("Multiple Gamepads") - func testMultipleGamepads() async { + mutating func testMultipleGamepads() async { let gamepadId0 = 0 let gamepadId1 = 1 let windowId = UIWindow.ID.empty diff --git a/Tests/AdaTransformTests/AdaTransformTests.swift b/Tests/AdaTransformTests/AdaTransformTests.swift index b3ceb5de6..5dc86e5bc 100644 --- a/Tests/AdaTransformTests/AdaTransformTests.swift +++ b/Tests/AdaTransformTests/AdaTransformTests.swift @@ -11,19 +11,19 @@ struct AdaTransformTests: Sendable { let world: AppWorlds init() async throws { - self.world = AppWorlds(mainWorld: World()) + self.world = AppWorlds(main: World()) + .addPlugin(MainSchedulerPlugin()) .addPlugin(TransformPlugin()) - try self.world.build() + try await self.world.build() } @Test("Global transform test") func globalTransformTest() async throws { - let entity = Entity() - entity.components += Transform() - self.world.addEntity(entity) - self.world.mainWorld.flush() - await world.mainWorld.runScheduler(.postUpdate, deltaTime: 1 / 60) + let entity = world.main.spawn { + Transform() + } + await world.main.runScheduler(.postUpdate) let globalTransform = try #require(entity.components[GlobalTransform.self]) #expect(globalTransform.matrix == Transform3D.identity) @@ -31,18 +31,14 @@ struct AdaTransformTests: Sendable { @Test("Parent-child transform propagation test") func parentChildTransformPropagationTest() async throws { - let parent = Entity() - parent.components += Transform(position: Vector3(x: 10, y: 20, z: 30)) - - let child = Entity() - let childLocalTransform = Transform(position: Vector3(x: 5, y: 0, z: 0)) - child.components += childLocalTransform + let parent = world.main.spawn { + Transform(position: Vector3(x: 10, y: 20, z: 30)) + } + let child = world.main.spawn { + Transform(position: Vector3(x: 5, y: 0, z: 0)) + } parent.addChild(child) - - self.world.addEntity(parent) - self.world.addEntity(child) - self.world.mainWorld.flush() - await world.mainWorld.runScheduler(.postUpdate, deltaTime: 1 / 60) + await world.main.runScheduler(.postUpdate) let childGlobalTransform = try #require(child.components[GlobalTransform.self]) let expectedChildGlobalPosition = Vector3(x: 15, y: 20, z: 30) // parent position + child local position @@ -51,9 +47,9 @@ struct AdaTransformTests: Sendable { // Test moving parent parent.components[Transform.self]?.position = Vector3(x: 100, y: 200, z: 300) - self.world.mainWorld.flush() + self.world.main.flush() - await world.mainWorld.runScheduler(.postUpdate, deltaTime: 1 / 60) + await world.main.runScheduler(.postUpdate) let updatedChildGlobalTransform = try #require(child.components[GlobalTransform.self]) let expectedUpdatedChildGlobalPosition = Vector3(x: 105, y: 200, z: 300) // new parent position + child local position @@ -61,9 +57,3 @@ struct AdaTransformTests: Sendable { #expect(updatedChildGlobalTransform.getTransform().position == expectedUpdatedChildGlobalPosition) } } - -extension World { - func update() async { - await self.update(1 / 60) - } -} diff --git a/Tests/AdaUtilsTests/BUILD.bazel b/Tests/AdaUtilsTests/BUILD.bazel new file mode 100644 index 000000000..eaf374621 --- /dev/null +++ b/Tests/AdaUtilsTests/BUILD.bazel @@ -0,0 +1,12 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_test") + +swift_test( + name = "AdaUtilsTests", + srcs = [ + "QueryTests.swift", + ], + module_name = "AdaUtilsTests", + visibility = ["//visibility:public"], + deps = ["//Sources/AdaUtils"], +) + diff --git a/Tests/AdaUtilsTests/SparseArrayTests.swift b/Tests/AdaUtilsTests/SparseArrayTests.swift new file mode 100644 index 000000000..15841a86d --- /dev/null +++ b/Tests/AdaUtilsTests/SparseArrayTests.swift @@ -0,0 +1,123 @@ +// +// SparseArrayTests.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 04.11.2025. +// + +@testable import AdaUtils +import Testing + +@Suite +struct SparseArrayTests { + @Test + func `init with capacity contains same count of allocated elements`() throws { + let array = SparseArray(capacity: 30) + #expect(array.isEmpty == true) + #expect(array.count == 0) + #expect(array.underestimatedCount == 30) + } + + @Test + func `init with sequence contains same count of allocated elements`() throws { + let array = SparseArray([10, 20, 40]) + #expect(array.isEmpty == false) + #expect(array.count == 3) + #expect(array.underestimatedCount == 3) + } + + @Test + func `allocation with literal`() throws { + let array: SparseArray = [10, 20, 30] + #expect(array.isEmpty == false) + #expect(array.count == 3) + #expect(array.underestimatedCount == 3) + } + + @Test + func `remove all with keepingCapacity contains same count of allocated elements`() throws { + var array = SparseArray(capacity: 30) + #expect(array.isEmpty == true) + #expect(array.count == 0) + #expect(array.underestimatedCount == 30) + + array.removeAll(keepingCapacity: true) + + #expect(array.isEmpty == true) + #expect(array.count == 0) + #expect(array.underestimatedCount == 30) + } + + @Test + func `remove all without keepingCapacity is empty`() throws { + var array = SparseArray(capacity: 30) + #expect(array.isEmpty == true) + #expect(array.count == 0) + #expect(array.underestimatedCount == 30) + + array.removeAll(keepingCapacity: false) + + #expect(array.isEmpty == true) + #expect(array.count == 0) + #expect(array.underestimatedCount == 0) + } + + @Test + func `remove at index replace value by nil`() throws { + var array = SparseArray([10, 20, 30]) + let value = array.remove(at: 1) + #expect(value == 20) + #expect(array.values[1] == nil) + #expect(array.count == 2) + #expect(array.underestimatedCount == 3) + } + + @Test + func `remove last returns value and replace last by nil`() throws { + var array = SparseArray([10, 20, 30, 40]) + let value = array.remove(at: 3) + #expect(value == 40) + + let removedLast = array.removeLast() + + // should be [10, 20, nil, nil] + #expect(removedLast == 30) + #expect(array.values[2] == nil) + #expect(array.count == 2) + #expect(array.underestimatedCount == 4) + } + + @Test + func `insert element at index works properly`() throws { + var array = SparseArray([10, 20, 30, 40]) + array.insert(50, at: 1) + #expect(array.values == [10, 50, 30, 40]) + } + + @Test + func `append element works properly`() throws { + var array = SparseArray([10, 20, 30, 40]) + array.append(50) + #expect(array.values.prefix(5) == [10, 20, 30, 40, 50]) + #expect(array.isEmpty == false) + #expect(array.count == 5) + #expect(array.underestimatedCount == 20) + } + + @Test + func `iterator never returns nil values`() async throws { + var array = SparseArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + array.remove(at: 1) + array.remove(at: 5) + array.remove(at: 9) + + var expectedSequence = [1, 3, 4, 5, 7, 8, 9] + #expect(array.count == expectedSequence.count) + #expect(array.underestimatedCount == 10) + + for element in array { + #expect(expectedSequence.removeFirst() == element) + } + #expect(expectedSequence.count == 0) + } +} diff --git a/Tests/AdaUtilsTests/SparseSetTests.swift b/Tests/AdaUtilsTests/SparseSetTests.swift new file mode 100644 index 000000000..17a980f87 --- /dev/null +++ b/Tests/AdaUtilsTests/SparseSetTests.swift @@ -0,0 +1,75 @@ +// +// SparseSetTests.swift +// AdaEngine +// +// Created by Vladislav Prusakov on 15.11.2025. +// + +import AdaUtils +import Testing + +@Suite +struct SparseSetTests { + @Test func `getting value from set returns correctly`() { + var sparseSet = SparseSet() + sparseSet[0] = "0" + sparseSet[1] = "1" + sparseSet[2] = "2" + sparseSet[3] = "3" + + #expect(sparseSet.count == 4) + #expect(sparseSet[0] == "0") + #expect(sparseSet[1] == "1") + #expect(sparseSet[2] == "2") + #expect(sparseSet[3] == "3") + } + + @Test func `iterator works as plain array`() { + var sparseSet = SparseSet() + sparseSet[0] = "0" + sparseSet[1] = "1" + sparseSet[2] = "2" + sparseSet[3] = "3" + + var expectedArray = ["0", "1", "2", "3"] + + for value in sparseSet { + #expect(expectedArray.removeFirst() == value) + } + + #expect(expectedArray.isEmpty == true) + } + + @Test func `remove element works as expected`() { + var sparseSet = SparseSet() + sparseSet[0] = "0" + sparseSet[1] = "1" + sparseSet[2] = "2" + sparseSet[3] = "3" + + #expect(sparseSet.count == 4) + var expectedArray = ["0", "1", "3"] + sparseSet.remove(for: 2) + + #expect(sparseSet.count == 3) + + for value in sparseSet { + #expect(expectedArray.removeFirst() == value) + } + + #expect(expectedArray.isEmpty == true) + } + + @Test func `remove non existing element return nil`() { + var sparseSet = SparseSet() + sparseSet[0] = "0" + sparseSet[1] = "1" + sparseSet[2] = "2" + sparseSet[3] = "3" + #expect(sparseSet.count == 4) + + let value = sparseSet.remove(for: 9) + #expect(value == nil) + #expect(sparseSet.count == 4) + } +} diff --git a/Tests/AdaUtilsTests/UniqueHashableTests.swift b/Tests/AdaUtilsTests/UniqueHashableTests.swift new file mode 100644 index 000000000..ae2d95e34 --- /dev/null +++ b/Tests/AdaUtilsTests/UniqueHashableTests.swift @@ -0,0 +1,102 @@ +@testable import AdaUtils +import Testing + +@Suite +struct UniqueHashableTests { + + @Test + func `consistency of FNVHasher with String`() { + let str1 = "Hello, AdaEngine!" + let str2 = "Hello, AdaEngine!" + let str3 = "Different String" + + var hasher1 = FNVHasher() + str1.hash(into: &hasher1) + let hash1 = hasher1.finalize() + + var hasher2 = FNVHasher() + str2.hash(into: &hasher2) + let hash2 = hasher2.finalize() + + var hasher3 = FNVHasher() + str3.hash(into: &hasher3) + let hash3 = hasher3.finalize() + + #expect(hash1 == hash2) + #expect(hash1 != hash3) + + // Test convenience property + #expect(str1.uniqueHashValue == str2.uniqueHashValue) + #expect(str1.uniqueHashValue != str3.uniqueHashValue) + } + + @Test("Test integer hashing") + func testIntegerHashing() { + let int1: Int = 42 + let int2: Int = 42 + let int3: Int = 100 + + #expect(int1.uniqueHashValue == int2.uniqueHashValue) + #expect(int1.uniqueHashValue != int3.uniqueHashValue) + } + + @Test + func `float and double hashing`() { + let double1: Double = 3.14159 + let double2: Double = 3.14159 + let double3: Double = 2.71828 + + #expect(double1.uniqueHashValue == double2.uniqueHashValue) + #expect(double1.uniqueHashValue != double3.uniqueHashValue) + } + + @Test + func `primitive types consistency`() { + let u8: UInt8 = 255 + let u32: UInt32 = 123456 + let u64: UInt64 = 12345678901234 + + #expect(u8.uniqueHashValue == u8.uniqueHashValue) + #expect(u32.uniqueHashValue == u32.uniqueHashValue) + #expect(u64.uniqueHashValue == u64.uniqueHashValue) + } + + @Test + func `hasher combine mixed types`() { + var hasher1 = FNVHasher() + hasher1.combine(42) + hasher1.combine("Ada") + let result1 = hasher1.finalize() + + var hasher2 = FNVHasher() + hasher2.combine(42) + hasher2.combine("Ada") + let result2 = hasher2.finalize() + + var hasher3 = FNVHasher() + hasher3.combine(42) + hasher3.combine("Beta") + let result3 = hasher3.finalize() + + #expect(result1 == result2) + #expect(result1 != result3) + } + + @Test + func `same hash for string between launches`() { + let hash1 = "Hello, AdaEngine!".uniqueHashValue + #expect(hash1 == 6458726915318513084) + } + + @Test + func `same hash for double between launches`() { + let double1: Double = 3.14159 + #expect(double1.uniqueHashValue == -1627972450653492632) + } + + @Test + func `same hash for int between launches`() { + let int1: Int = 42 + #expect(int1.uniqueHashValue == -55488592825689361) + } +}