diff --git a/.github/guide-source-anchors.json b/.github/guide-source-anchors.json index 32821b6..f577af4 100644 --- a/.github/guide-source-anchors.json +++ b/.github/guide-source-anchors.json @@ -22,7 +22,7 @@ "sim/compute_backend.v#L785": "pub struct ParticleMotionSoa {", "sim/compute_backend.v#L834": "pub fn cpu_step_particle_motion(mut particles ParticleMotionSoa) bool {", "sim/compute_backend.v#L86": "pub fn shot_bullet_collision_candidates(shots []Shot, bullets []Bullet) []ShotCollisionCandidate {", - "sim/compute_backend_test.v#L267": "fn test_requested_opencl_backend_falls_back_to_identical_cpu_results() {", + "sim/compute_backend_test.v#L271": "fn test_requested_opencl_backend_falls_back_to_identical_cpu_results() {", "sim/course.v#L161": "pub fn generate_course(seed u32) CourseProfile {", "sim/course.v#L288": "pub fn (course &CourseProfile) slice_at(position f32) CourseSlice {", "sim/course.v#L299": "pub fn course_side(angle f32, slice CourseSlice) int {", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2643276..a4bb1af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,7 +187,11 @@ jobs: # call the optional OpenCL path. Keep those declarations for this suite. run: python3 scripts/check_compiler_diagnostics.py -- v -no-skip-unused -d opencl_compute -cc gcc test sim - name: Compile OpenCL backend with TinyCC - run: python3 scripts/check_compiler_diagnostics.py -- v -d opencl_compute -cc tcc -o torus_trooper_opencl_tcc . && ./torus_trooper_opencl_tcc --headless --ticks 120 --compute opencl | grep -F 'compute_active=opencl' + run: | + python3 scripts/check_compiler_diagnostics.py -- v -d opencl_compute -cc tcc -o torus_trooper_opencl_tcc . + ./torus_trooper_opencl_tcc --headless --ticks 120 --compute opencl | grep -F 'compute_active=opencl' + python3 scripts/check_compiler_diagnostics.py -- v -d opencl_compute -d opencl_dynamic -cc tcc -o torus_trooper_opencl_dynamic_tcc . + python3 scripts/test_opencl_runtime.py ./torus_trooper_opencl_dynamic_tcc --require-opencl - name: Verify deterministic OpenCL headless run run: xvfb-run -a env TT_OPENCL_SMOKE=1 TT_REPLAY_UI_SMOKE=1 ./scripts/check.sh - name: Package Linux candidate @@ -349,6 +353,26 @@ jobs: $env:GLFW_LIB = Join-Path $GlfwRoot 'lib' $Executable = Join-Path $env:RUNNER_TEMP 'torus_trooper.exe' .\scripts\build_windows.ps1 -OutputPath $Executable -Verify + - name: Verify optional OpenCL startup on Windows + shell: pwsh + run: | + $HeadersRef = (Get-Content modules/antono2/opencl/HEADERS_COMMIT -Raw).Trim() + if ($HeadersRef -notmatch '^[0-9a-f]{40}$') { throw 'Invalid OpenCL header revision' } + $HeadersRoot = Join-Path $env:RUNNER_TEMP 'opencl-headers' + git init $HeadersRoot + git -C $HeadersRoot fetch --depth 1 https://github.com/KhronosGroup/OpenCL-Headers.git $HeadersRef + if ($LASTEXITCODE -ne 0) { throw 'Cannot fetch pinned OpenCL headers' } + git -C $HeadersRoot checkout --detach FETCH_HEAD + if ($LASTEXITCODE -ne 0) { throw 'Cannot check out pinned OpenCL headers' } + Copy-Item (Join-Path $HeadersRoot 'CL/*') modules/antono2/opencl/include/CL -Force + $GlfwRoot = Join-Path $env:VCPKG_INSTALLATION_ROOT 'installed/x64-mingw-static' + $env:GLFW_INCLUDE = Join-Path $GlfwRoot 'include' + $env:GLFW_LIB = Join-Path $GlfwRoot 'lib' + $DynamicExecutable = Join-Path $env:RUNNER_TEMP 'torus-opencl-dynamic.exe' + python scripts/check_compiler_diagnostics.py -- v -subsystem console -d opencl_compute -d opencl_dynamic -cc clang -cflags -Wno-incompatible-pointer-types -o $DynamicExecutable . + if ($LASTEXITCODE -ne 0) { throw 'Dynamic OpenCL compilation failed' } + python scripts/test_opencl_runtime.py $DynamicExecutable + if ($LASTEXITCODE -ne 0) { throw 'Dynamic OpenCL fallback verification failed' } - name: Package and probe Windows candidate shell: pwsh run: | diff --git a/compatibility.json b/compatibility.json index 416100d..dc49838 100644 --- a/compatibility.json +++ b/compatibility.json @@ -15,7 +15,7 @@ "ci_sdk": "1.4.309.0", "ci_runtime": "v1.2", "modules": { - "opencl": "v1.0.2", + "opencl": "v1.1.0", "vulkan": "v3.2.2", "vkmemalloc": "v2.6.2", "memory": "v1.4.0" diff --git a/docs/guide/07-compute-and-verification.md b/docs/guide/07-compute-and-verification.md index 2b0caee..92b9990 100644 --- a/docs/guide/07-compute-and-verification.md +++ b/docs/guide/07-compute-and-verification.md @@ -56,6 +56,13 @@ records why. An OpenCL build can also fall back when no device is available or when initialization or dispatch fails. The session owns its device resources and `App.shutdown` closes it. +The [dynamic build](../technical-reference.md#optional-opencl-backend) also +reaches this fallback when the OpenCL loader is absent, before device discovery. +Available devices must support source compilation; selection prefers a GPU and +reports why candidates were rejected. Reusable scratch owners grow buffers only +after replacement allocation succeeds and release their retained context +references during cleanup. + Headless output reports the requested and active backend, verified and mismatched batches, fallback counts, per-workload mismatches, collision candidate counts and a compute checksum. Read these numbers together with @@ -96,7 +103,7 @@ the CPU fallback remains part of the supported configuration. The ordinary binary reports `compute_requested=opencl` but `compute_active=cpu` when built without `-d opencl_compute`. The -[`compute_backend_test.v`](../../sim/compute_backend_test.v#L267) and +[`compute_backend_test.v`](../../sim/compute_backend_test.v#L271) and [`scripts/check.sh`](../../scripts/check.sh) verify that fallback preserves gameplay checksums. The optional `TT_OPENCL_SMOKE=1` gate requires a device run and checks its batches. Those are correctness checks, not benchmarks. diff --git a/docs/technical-reference.md b/docs/technical-reference.md index 469c5bd..fa5fcdb 100644 --- a/docs/technical-reference.md +++ b/docs/technical-reference.md @@ -153,7 +153,7 @@ directly. ### Optional OpenCL backend ```sh -v -d opencl_compute -cc gcc -o torus_trooper . +v -d opencl_compute -d opencl_dynamic -cc gcc -o torus_trooper . ./torus_trooper --compute opencl ``` @@ -162,6 +162,13 @@ or produces a batch that differs from the CPU reference, the game uses the CPU result and reports the fallback. See [Compute backends](#compute-backends) for what is checked and which work remains on the CPU. +`-d opencl_dynamic` loads OpenCL at runtime, so an enabled executable can still +start and use the CPU when the loader is missing. Development headers are needed +at build time. Omit this flag to link the loader directly. Set `OPENCL_LIBRARY` +to an absolute library path to select a specific runtime; a failed override is +reported and falls back to CPU without trying another loader. Device selection +prefers an available GPU with a source compiler, then another capable device. + ## Advanced settings and controls ### Option files and saved preferences diff --git a/scripts/check.sh b/scripts/check.sh index df8bb0e..0ef567f 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -77,6 +77,10 @@ if [[ "${TT_OPENCL_SMOKE:-0}" == "1" ]]; then grep -Fq 'compute_shot_mismatched_batches=0' <<<"$opencl_output" grep -Fq 'compute_enemy_mismatched_batches=0' <<<"$opencl_output" grep -Fq 'compute_checksum=3df697f628fcafc9' <<<"$opencl_output" + + dynamic_binary="$build_dir/torus_trooper_opencl_dynamic" + python3 scripts/check_compiler_diagnostics.py -- v -d opencl_compute -d opencl_dynamic -cc gcc -o "$dynamic_binary" . + python3 scripts/test_opencl_runtime.py "$dynamic_binary" --require-opencl fi probe_output=$("$binary" --no-sound --volume 0 --res 800 600 --reverse --brightness 70 --luminosity 40 --probe) diff --git a/scripts/test_opencl_runtime.py b/scripts/test_opencl_runtime.py new file mode 100644 index 0000000..3a84892 --- /dev/null +++ b/scripts/test_opencl_runtime.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Verify optional OpenCL loading and deterministic fallback in a built game.""" + +import argparse +import os +from pathlib import Path +import subprocess +import sys +import tempfile + + +def run(binary, backend, environment): + result = subprocess.run( + [str(binary), '--headless', '--ticks', '600', '--compute', backend, + '--no-sound', '--volume', '0'], + env=environment, capture_output=True, text=True, timeout=60, check=True, + ) + fields = dict(line.split('=', 1) for line in result.stdout.splitlines() if '=' in line) + assert fields['compute_requested'] == backend, result.stdout + return fields + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('binary', type=Path) + parser.add_argument('--require-opencl', action='store_true') + args = parser.parse_args() + binary = args.binary.resolve() + if sys.platform == 'linux': + linked = subprocess.check_output(['ldd', str(binary)], text=True) + assert 'libOpenCL' not in linked, linked + + environment = dict(os.environ) + environment.pop('OPENCL_LIBRARY', None) + cpu = run(binary, 'cpu', environment) + available = run(binary, 'opencl', environment) + if args.require_opencl: + assert available['compute_active'] == 'opencl', available + assert int(available['compute_verified_batches']) > 0, available + + with tempfile.TemporaryDirectory() as temporary: + missing = run(binary, 'opencl', dict( + environment, OPENCL_LIBRARY=str(Path(temporary) / 'missing-opencl-runtime'), + )) + assert missing['compute_active'] == 'cpu', missing + assert 'OpenCL runtime unavailable' in missing['opencl_fallback'], missing + assert int(missing['compute_fallback_batches']) > 0, missing + assert missing['compute_verified_batches'] == '0', missing + checksums = ['checksum', 'render_checksum', 'course_checksum', + 'course_fill_checksum', 'course_backward_checksum', + 'course_backward_fill_checksum', 'compute_checksum'] + for key in checksums: + assert missing[key] == cpu[key], (key, cpu[key], missing[key]) + assert available[key] == cpu[key], (key, cpu[key], available[key]) + print('Dynamic OpenCL execution and missing-loader CPU fallback preserve all checksums.') + + +if __name__ == '__main__': + main() diff --git a/sim/compute_backend_test.v b/sim/compute_backend_test.v index 019f0c1..e98abb9 100644 --- a/sim/compute_backend_test.v +++ b/sim/compute_backend_test.v @@ -3,6 +3,10 @@ module sim import math +$if opencl_compute ? { + import antono2.opencl as cl +} + fn test_cpu_particle_compute_matches_scalar_reference_and_compacts_slots() { mut simulation := new_simulation(SimulationConfig{ particle_capacity: 4 }) simulation.particles[1] = Particle{ @@ -300,6 +304,33 @@ fn test_compute_session_explains_an_unlinked_opencl_fallback() { } $if opencl_compute ? { + fn opencl_context_references(context cl.Context) !u32 { + mut count := u32(0) + cl.check(cl.get_context_info(context, cl.context_reference_count, sizeof(count), &count, + unsafe { nil }), 'read test context reference count')! + return count + } + + fn test_opencl_initial_dispatch_error_releases_unadopted_scratch() ! { + mut compute := new_opencl_compute() or { + eprintln('OpenCL dispatch cleanup test skipped: ${err}') + return + } + defer { compute.close() } + before := opencl_context_references(compute.context.handle)! + closed_queue := cl.OwnedCommandQueue{} + mut items := [OpenCLParticleMotion{ life: 3 }] + mut failed := false + run_motion_kernel(&compute.context, &closed_queue, &compute.particle_kernel, + unsafe { &cl.ScratchBuffer[OpenCLParticleMotion](nil) }, mut items) or { + failed = true + assert err.msg().contains('closed queue') + } + assert failed + assert opencl_context_references(compute.context.handle)! == before + assert isnil(compute.particle_buffer) + } + fn test_opencl_session_matches_all_cpu_motion_kernels() { assert sizeof(OpenCLParticleSlot) == 24 assert sizeof(OpenCLBulletSlot) == 36 @@ -424,13 +455,13 @@ $if opencl_compute ? { assert batch.active_shots == 2 assert batch.active_targets == 3 assert batch.tested_pairs == 6 - assert compute.collision_shot_slots.count == shots.len - assert compute.collision_shots.count == 2 - assert compute.collision_target_slots.count == enemies.len - assert compute.collision_targets.count == 3 - assert compute.active_flags_a.count == 6 - assert compute.active_flags_b.count == 6 - assert compute.collision_candidate_buffer.count == 6 + assert compute.collision_shot_slots.buffer()!.count == shots.len + assert compute.collision_shots.buffer()!.count == 2 + assert compute.collision_target_slots.buffer()!.count == enemies.len + assert compute.collision_targets.buffer()!.count == 3 + assert compute.active_flags_a.buffer()!.count == 6 + assert compute.active_flags_b.buffer()!.count == 6 + assert compute.collision_candidate_buffer.buffer()!.count == 6 } fn test_opencl_generates_stable_shot_bullet_candidates() ! { @@ -483,14 +514,14 @@ $if opencl_compute ? { assert batch.active_shots == 1 assert batch.active_targets == 3 assert batch.tested_pairs == 3 - assert compute.collision_shot_slots.count == shots.len - assert compute.collision_shots.count == 1 - assert compute.collision_target_slots.count == bullets.len - assert compute.collision_targets.count == 3 + assert compute.collision_shot_slots.buffer()!.count == shots.len + assert compute.collision_shots.buffer()!.count == 1 + assert compute.collision_target_slots.buffer()!.count == bullets.len + assert compute.collision_targets.buffer()!.count == 3 // Scan buffers retain the largest slot count used earlier in the batch. - assert compute.active_flags_a.count == shots.len - assert compute.active_flags_b.count == shots.len - assert compute.collision_candidate_buffer.count == 3 + assert compute.active_flags_a.buffer()!.count == shots.len + assert compute.active_flags_b.buffer()!.count == shots.len + assert compute.collision_candidate_buffer.buffer()!.count == 3 } fn test_opencl_live_collision_candidate_validation_is_observational() { @@ -658,12 +689,25 @@ $if opencl_compute ? { lives: [3] } compute.step_particles(mut one)! - first_handle := compute.particle_buffer.handle + first_handle := compute.particle_buffer.buffer()!.handle assert !isnil(first_handle) - assert compute.particle_buffer.count == 1 + assert compute.particle_buffer.buffer()!.count == 1 compute.step_particles(mut one)! - assert compute.particle_buffer.handle == first_handle + assert compute.particle_buffer.buffer()!.handle == first_handle + + // A rejected reservation must leave the owner and its contents usable. + mut rejected := false + ensure_buffer_capacity(&compute.context, compute.particle_buffer, -1) or { + rejected = true + assert err.msg().contains('negative') + } + assert rejected + assert compute.particle_buffer.buffer()!.handle == first_handle + mut retained := []OpenCLParticleMotion{len: 1} + compute.particle_buffer.buffer()!.read(&compute.queue, 0, mut retained)! + assert retained[0].angle == one.angles[0] + assert retained[0].life == one.lives[0] mut three := ParticleMotionSoa{ source_indices: [0, 1, 2] @@ -674,7 +718,7 @@ $if opencl_compute ? { lives: [3, 4, 5] } compute.step_particles(mut three)! - assert compute.particle_buffer.count == 3 + assert compute.particle_buffer.buffer()!.count == 3 } fn test_opencl_compacts_particle_pool_in_stable_order() ! { @@ -714,10 +758,10 @@ $if opencl_compute ? { assert actual.source_indices == [1, 3, 6] assert compare_particle_motion(expected, actual, 0.00001).matches() - assert compute.particle_slots.count == 7 - assert compute.active_flags_a.count == 7 - assert compute.active_flags_b.count == 7 - assert compute.particle_indices.count == 7 + assert compute.particle_slots.buffer()!.count == 7 + assert compute.active_flags_a.buffer()!.count == 7 + assert compute.active_flags_b.buffer()!.count == 7 + assert compute.particle_indices.buffer()!.count == 7 } fn test_opencl_compacts_bullet_pool_in_stable_order() ! { @@ -763,8 +807,8 @@ $if opencl_compute ? { assert actual.source_indices == [0, 4, 6] assert compare_bullet_motion(expected, actual, 0.00001).matches() - assert compute.bullet_slots.count == 7 - assert compute.bullet_indices.count == 7 + assert compute.bullet_slots.buffer()!.count == 7 + assert compute.bullet_indices.buffer()!.count == 7 } fn test_opencl_compacts_only_released_shots_in_stable_order() ! { @@ -805,8 +849,8 @@ $if opencl_compute ? { assert actual.source_indices == [2, 5] assert compare_shot_motion(expected, actual, 0.00001).matches() - assert compute.shot_slots.count == 7 - assert compute.shot_indices.count == 7 + assert compute.shot_slots.buffer()!.count == 7 + assert compute.shot_indices.buffer()!.count == 7 } fn test_opencl_compacts_prepared_enemies_in_stable_order() ! { @@ -839,8 +883,8 @@ $if opencl_compute ? { assert actual.source_indices == [1, 5] assert compare_enemy_motion(expected, actual, 0.00001).matches() - assert compute.enemy_slots.count == 7 - assert compute.enemy_indices.count == 7 + assert compute.enemy_slots.buffer()!.count == 7 + assert compute.enemy_indices.buffer()!.count == 7 } } diff --git a/sim/opencl_compute.v b/sim/opencl_compute.v index 585aa00..fa5671b 100644 --- a/sim/opencl_compute.v +++ b/sim/opencl_compute.v @@ -439,40 +439,34 @@ kernel void step_enemies(global EnemyMotion *items) { bullet_kernel cl.OwnedKernel shot_kernel cl.OwnedKernel enemy_kernel cl.OwnedKernel - particle_slots &cl.Buffer[OpenCLParticleSlot] - active_flags_a &cl.Buffer[i32] - active_flags_b &cl.Buffer[i32] - particle_buffer &cl.Buffer[OpenCLParticleMotion] - particle_indices &cl.Buffer[i32] - bullet_slots &cl.Buffer[OpenCLBulletSlot] - bullet_buffer &cl.Buffer[OpenCLBulletMotion] - bullet_indices &cl.Buffer[i32] - shot_slots &cl.Buffer[OpenCLShotSlot] - shot_buffer &cl.Buffer[OpenCLShotMotion] - shot_indices &cl.Buffer[i32] - enemy_slots &cl.Buffer[OpenCLEnemySlot] - enemy_buffer &cl.Buffer[OpenCLEnemyMotion] - enemy_indices &cl.Buffer[i32] - collision_shot_slots &cl.Buffer[OpenCLCollisionPoint] - collision_shots &cl.Buffer[OpenCLCollisionPoint] - collision_target_slots &cl.Buffer[OpenCLCollisionPoint] - collision_targets &cl.Buffer[OpenCLCollisionPoint] - collision_candidate_buffer &cl.Buffer[OpenCLShotCollisionCandidate] + particle_slots &cl.ScratchBuffer[OpenCLParticleSlot] + active_flags_a &cl.ScratchBuffer[i32] + active_flags_b &cl.ScratchBuffer[i32] + particle_buffer &cl.ScratchBuffer[OpenCLParticleMotion] + particle_indices &cl.ScratchBuffer[i32] + bullet_slots &cl.ScratchBuffer[OpenCLBulletSlot] + bullet_buffer &cl.ScratchBuffer[OpenCLBulletMotion] + bullet_indices &cl.ScratchBuffer[i32] + shot_slots &cl.ScratchBuffer[OpenCLShotSlot] + shot_buffer &cl.ScratchBuffer[OpenCLShotMotion] + shot_indices &cl.ScratchBuffer[i32] + enemy_slots &cl.ScratchBuffer[OpenCLEnemySlot] + enemy_buffer &cl.ScratchBuffer[OpenCLEnemyMotion] + enemy_indices &cl.ScratchBuffer[i32] + collision_shot_slots &cl.ScratchBuffer[OpenCLCollisionPoint] + collision_shots &cl.ScratchBuffer[OpenCLCollisionPoint] + collision_target_slots &cl.ScratchBuffer[OpenCLCollisionPoint] + collision_targets &cl.ScratchBuffer[OpenCLCollisionPoint] + collision_candidate_buffer &cl.ScratchBuffer[OpenCLShotCollisionCandidate] device_name string } fn select_opencl_device() !(cl.DeviceId, string) { - platforms := cl.platforms()! - for device_kind in [cl.device_type_gpu, cl.device_type_all] { - for platform in platforms { - devices := cl.devices(platform, device_kind)! - if devices.len > 0 { - name := cl.device_info_string(devices[0], cl.device_name)! - return devices[0], name - } - } - } - return error('no OpenCL devices are available') + device := cl.select_device(cl.DeviceRequirements{ + // Preserve GPU preference and allow other capable device types. + preferred_types: [cl.device_type_gpu, cl.device_type_all] + })! + return device.device, device.name } fn new_opencl_compute() !&OpenCLCompute { @@ -582,41 +576,53 @@ kernel void step_enemies(global EnemyMotion *items) { return compute } - fn ensure_buffer_capacity[T](context &cl.OwnedContext, buffer &cl.Buffer[T], count int) !&cl.Buffer[T] { - if !isnil(buffer) && buffer.count >= count { - return unsafe { buffer } - } + fn ensure_buffer_capacity[T](context &cl.OwnedContext, buffer &cl.ScratchBuffer[T], count int) !&cl.ScratchBuffer[T] { if !isnil(buffer) { - mut previous := unsafe { buffer } - previous.close()! + mut scratch := unsafe { buffer } + scratch.reserve(count)! + return scratch + } + mut scratch := cl.new_scratch_buffer[T](context, cl.mem_read_write)! + scratch.reserve(count) or { + scratch.close() or {} + return err } - return cl.new_buffer[T](context, cl.mem_read_write, count)! + return scratch } - fn close_buffer_if_allocated[T](mut buffer &cl.Buffer[T]) { + fn close_buffer_if_allocated[T](mut buffer &cl.ScratchBuffer[T]) { if !isnil(buffer) { buffer.close() or {} } } fn run_motion_kernel[T](context &cl.OwnedContext, queue &cl.OwnedCommandQueue, - kernel &cl.OwnedKernel, buffer &cl.Buffer[T], mut items []T) !&cl.Buffer[T] { + kernel &cl.OwnedKernel, buffer &cl.ScratchBuffer[T], mut items []T) !&cl.ScratchBuffer[T] { if items.len == 0 { return unsafe { buffer } } active_buffer := ensure_buffer_capacity(context, buffer, items.len)! - active_buffer.write(queue, 0, items)! - kernel.set_buffer_arg(0, active_buffer.handle)! + mut keep := false + defer { + // The caller cannot adopt a newly created owner after a dispatch error. + if isnil(buffer) && !keep { + mut pending := unsafe { active_buffer } + pending.close() or {} + } + } + active_buffer.buffer()!.write(queue, 0, items)! + kernel.set_buffer_arg(0, active_buffer.buffer()!.handle)! kernel.enqueue_1d(queue, usize(items.len), 0)! - active_buffer.read(queue, 0, mut items)! + active_buffer.buffer()!.read(queue, 0, mut items)! + keep = true return active_buffer } fn (mut compute OpenCLCompute) scan_flags(count int) !(cl.Mem, int) { compute.active_flags_a = ensure_buffer_capacity(&compute.context, compute.active_flags_a, count)! compute.active_flags_b = ensure_buffer_capacity(&compute.context, compute.active_flags_b, count)! - mut scan_input := compute.active_flags_a.handle - mut scan_output := compute.active_flags_b.handle + mut scan_input := compute.active_flags_a.buffer()!.handle + mut scan_output := compute.active_flags_b.buffer()!.handle mut offset := 1 for offset < count { scan_offset := i32(offset) @@ -628,10 +634,10 @@ kernel void step_enemies(global EnemyMotion *items) { offset *= 2 } mut active_count_value := [i32(0)] - if scan_input == compute.active_flags_a.handle { - compute.active_flags_a.read(&compute.queue, count - 1, mut active_count_value)! + if scan_input == compute.active_flags_a.buffer()!.handle { + compute.active_flags_a.buffer()!.read(&compute.queue, count - 1, mut active_count_value)! } else { - compute.active_flags_b.read(&compute.queue, count - 1, mut active_count_value)! + compute.active_flags_b.buffer()!.read(&compute.queue, count - 1, mut active_count_value)! } active_count := int(active_count_value[0]) if active_count < 0 || active_count > count { @@ -644,7 +650,7 @@ kernel void step_enemies(global EnemyMotion *items) { slots cl.Mem, count int) !(cl.Mem, int) { compute.active_flags_a = ensure_buffer_capacity(&compute.context, compute.active_flags_a, count)! mark_kernel.set_buffer_arg(0, slots)! - mark_kernel.set_buffer_arg(1, compute.active_flags_a.handle)! + mark_kernel.set_buffer_arg(1, compute.active_flags_a.buffer()!.handle)! mark_kernel.enqueue_1d(&compute.queue, usize(count), 0)! return compute.scan_flags(count) } @@ -667,22 +673,22 @@ kernel void step_enemies(global EnemyMotion *items) { compute.particle_slots = ensure_buffer_capacity(&compute.context, compute.particle_slots, slots.len)! compute.particle_buffer = ensure_buffer_capacity(&compute.context, compute.particle_buffer, slots.len)! compute.particle_indices = ensure_buffer_capacity(&compute.context, compute.particle_indices, slots.len)! - compute.particle_slots.write(&compute.queue, 0, slots)! - scan, active_count := compute.scan_active_slots(&compute.particle_mark_kernel, compute.particle_slots.handle, slots.len)! + compute.particle_slots.buffer()!.write(&compute.queue, 0, slots)! + scan, active_count := compute.scan_active_slots(&compute.particle_mark_kernel, compute.particle_slots.buffer()!.handle, slots.len)! if active_count == 0 { return ParticleMotionSoa{} } - compute.particle_scatter_kernel.set_buffer_arg(0, compute.particle_slots.handle)! + compute.particle_scatter_kernel.set_buffer_arg(0, compute.particle_slots.buffer()!.handle)! compute.particle_scatter_kernel.set_buffer_arg(1, scan)! - compute.particle_scatter_kernel.set_buffer_arg(2, compute.particle_buffer.handle)! - compute.particle_scatter_kernel.set_buffer_arg(3, compute.particle_indices.handle)! + compute.particle_scatter_kernel.set_buffer_arg(2, compute.particle_buffer.buffer()!.handle)! + compute.particle_scatter_kernel.set_buffer_arg(3, compute.particle_indices.buffer()!.handle)! compute.particle_scatter_kernel.enqueue_1d(&compute.queue, usize(slots.len), 0)! - compute.particle_kernel.set_buffer_arg(0, compute.particle_buffer.handle)! + compute.particle_kernel.set_buffer_arg(0, compute.particle_buffer.buffer()!.handle)! compute.particle_kernel.enqueue_1d(&compute.queue, usize(active_count), 0)! mut packed := []OpenCLParticleMotion{len: active_count} mut source_indices := []i32{len: active_count} - compute.particle_buffer.read(&compute.queue, 0, mut packed)! - compute.particle_indices.read(&compute.queue, 0, mut source_indices)! + compute.particle_buffer.buffer()!.read(&compute.queue, 0, mut packed)! + compute.particle_indices.buffer()!.read(&compute.queue, 0, mut source_indices)! mut result := ParticleMotionSoa{ source_indices: []int{cap: active_count} angles: []f32{cap: active_count} @@ -747,22 +753,22 @@ kernel void step_enemies(global EnemyMotion *items) { compute.bullet_slots = ensure_buffer_capacity(&compute.context, compute.bullet_slots, slots.len)! compute.bullet_buffer = ensure_buffer_capacity(&compute.context, compute.bullet_buffer, slots.len)! compute.bullet_indices = ensure_buffer_capacity(&compute.context, compute.bullet_indices, slots.len)! - compute.bullet_slots.write(&compute.queue, 0, slots)! - scan, active_count := compute.scan_active_slots(&compute.bullet_mark_kernel, compute.bullet_slots.handle, slots.len)! + compute.bullet_slots.buffer()!.write(&compute.queue, 0, slots)! + scan, active_count := compute.scan_active_slots(&compute.bullet_mark_kernel, compute.bullet_slots.buffer()!.handle, slots.len)! if active_count == 0 { return BulletMotionSoa{} } - compute.bullet_scatter_kernel.set_buffer_arg(0, compute.bullet_slots.handle)! + compute.bullet_scatter_kernel.set_buffer_arg(0, compute.bullet_slots.buffer()!.handle)! compute.bullet_scatter_kernel.set_buffer_arg(1, scan)! - compute.bullet_scatter_kernel.set_buffer_arg(2, compute.bullet_buffer.handle)! - compute.bullet_scatter_kernel.set_buffer_arg(3, compute.bullet_indices.handle)! + compute.bullet_scatter_kernel.set_buffer_arg(2, compute.bullet_buffer.buffer()!.handle)! + compute.bullet_scatter_kernel.set_buffer_arg(3, compute.bullet_indices.buffer()!.handle)! compute.bullet_scatter_kernel.enqueue_1d(&compute.queue, usize(slots.len), 0)! - compute.bullet_kernel.set_buffer_arg(0, compute.bullet_buffer.handle)! + compute.bullet_kernel.set_buffer_arg(0, compute.bullet_buffer.buffer()!.handle)! compute.bullet_kernel.enqueue_1d(&compute.queue, usize(active_count), 0)! mut packed := []OpenCLBulletMotion{len: active_count} mut source_indices := []i32{len: active_count} - compute.bullet_buffer.read(&compute.queue, 0, mut packed)! - compute.bullet_indices.read(&compute.queue, 0, mut source_indices)! + compute.bullet_buffer.buffer()!.read(&compute.queue, 0, mut packed)! + compute.bullet_indices.buffer()!.read(&compute.queue, 0, mut source_indices)! mut result := BulletMotionSoa{ source_indices: []int{cap: active_count} angles: []f32{cap: active_count} @@ -831,22 +837,22 @@ kernel void step_enemies(global EnemyMotion *items) { compute.shot_slots = ensure_buffer_capacity(&compute.context, compute.shot_slots, slots.len)! compute.shot_buffer = ensure_buffer_capacity(&compute.context, compute.shot_buffer, slots.len)! compute.shot_indices = ensure_buffer_capacity(&compute.context, compute.shot_indices, slots.len)! - compute.shot_slots.write(&compute.queue, 0, slots)! - scan, active_count := compute.scan_active_slots(&compute.shot_mark_kernel, compute.shot_slots.handle, slots.len)! + compute.shot_slots.buffer()!.write(&compute.queue, 0, slots)! + scan, active_count := compute.scan_active_slots(&compute.shot_mark_kernel, compute.shot_slots.buffer()!.handle, slots.len)! if active_count == 0 { return ShotMotionSoa{} } - compute.shot_scatter_kernel.set_buffer_arg(0, compute.shot_slots.handle)! + compute.shot_scatter_kernel.set_buffer_arg(0, compute.shot_slots.buffer()!.handle)! compute.shot_scatter_kernel.set_buffer_arg(1, scan)! - compute.shot_scatter_kernel.set_buffer_arg(2, compute.shot_buffer.handle)! - compute.shot_scatter_kernel.set_buffer_arg(3, compute.shot_indices.handle)! + compute.shot_scatter_kernel.set_buffer_arg(2, compute.shot_buffer.buffer()!.handle)! + compute.shot_scatter_kernel.set_buffer_arg(3, compute.shot_indices.buffer()!.handle)! compute.shot_scatter_kernel.enqueue_1d(&compute.queue, usize(slots.len), 0)! - compute.shot_kernel.set_buffer_arg(0, compute.shot_buffer.handle)! + compute.shot_kernel.set_buffer_arg(0, compute.shot_buffer.buffer()!.handle)! compute.shot_kernel.enqueue_1d(&compute.queue, usize(active_count), 0)! mut packed := []OpenCLShotMotion{len: active_count} mut source_indices := []i32{len: active_count} - compute.shot_buffer.read(&compute.queue, 0, mut packed)! - compute.shot_indices.read(&compute.queue, 0, mut source_indices)! + compute.shot_buffer.buffer()!.read(&compute.queue, 0, mut packed)! + compute.shot_indices.buffer()!.read(&compute.queue, 0, mut source_indices)! mut result := ShotMotionSoa{ source_indices: []int{cap: active_count} angles: []f32{cap: active_count} @@ -907,22 +913,22 @@ kernel void step_enemies(global EnemyMotion *items) { compute.enemy_slots = ensure_buffer_capacity(&compute.context, compute.enemy_slots, slots.len)! compute.enemy_buffer = ensure_buffer_capacity(&compute.context, compute.enemy_buffer, slots.len)! compute.enemy_indices = ensure_buffer_capacity(&compute.context, compute.enemy_indices, slots.len)! - compute.enemy_slots.write(&compute.queue, 0, slots)! - scan, active_count := compute.scan_active_slots(&compute.enemy_mark_kernel, compute.enemy_slots.handle, slots.len)! + compute.enemy_slots.buffer()!.write(&compute.queue, 0, slots)! + scan, active_count := compute.scan_active_slots(&compute.enemy_mark_kernel, compute.enemy_slots.buffer()!.handle, slots.len)! if active_count == 0 { return EnemyMotionSoa{} } - compute.enemy_scatter_kernel.set_buffer_arg(0, compute.enemy_slots.handle)! + compute.enemy_scatter_kernel.set_buffer_arg(0, compute.enemy_slots.buffer()!.handle)! compute.enemy_scatter_kernel.set_buffer_arg(1, scan)! - compute.enemy_scatter_kernel.set_buffer_arg(2, compute.enemy_buffer.handle)! - compute.enemy_scatter_kernel.set_buffer_arg(3, compute.enemy_indices.handle)! + compute.enemy_scatter_kernel.set_buffer_arg(2, compute.enemy_buffer.buffer()!.handle)! + compute.enemy_scatter_kernel.set_buffer_arg(3, compute.enemy_indices.buffer()!.handle)! compute.enemy_scatter_kernel.enqueue_1d(&compute.queue, usize(slots.len), 0)! - compute.enemy_kernel.set_buffer_arg(0, compute.enemy_buffer.handle)! + compute.enemy_kernel.set_buffer_arg(0, compute.enemy_buffer.buffer()!.handle)! compute.enemy_kernel.enqueue_1d(&compute.queue, usize(active_count), 0)! mut packed := []OpenCLEnemyMotion{len: active_count} mut source_indices := []i32{len: active_count} - compute.enemy_buffer.read(&compute.queue, 0, mut packed)! - compute.enemy_indices.read(&compute.queue, 0, mut source_indices)! + compute.enemy_buffer.buffer()!.read(&compute.queue, 0, mut packed)! + compute.enemy_indices.buffer()!.read(&compute.queue, 0, mut source_indices)! mut result := EnemyMotionSoa{ source_indices: []int{cap: active_count} angles: []f32{cap: active_count} @@ -965,25 +971,33 @@ kernel void step_enemies(global EnemyMotion *items) { } fn (mut compute OpenCLCompute) compact_collision_points(points []OpenCLCollisionPoint, - slots &cl.Buffer[OpenCLCollisionPoint], - active_points &cl.Buffer[OpenCLCollisionPoint]) !(int, &cl.Buffer[OpenCLCollisionPoint]) { + slots &cl.ScratchBuffer[OpenCLCollisionPoint], + active_points &cl.ScratchBuffer[OpenCLCollisionPoint]) !(int, &cl.ScratchBuffer[OpenCLCollisionPoint]) { if points.len == 0 { return 0, unsafe { active_points } } compute.active_flags_a = ensure_buffer_capacity(&compute.context, compute.active_flags_a, points.len)! - slots.write(&compute.queue, 0, points)! - compute.collision_point_mark_kernel.set_buffer_arg(0, slots.handle)! - compute.collision_point_mark_kernel.set_buffer_arg(1, compute.active_flags_a.handle)! + slots.buffer()!.write(&compute.queue, 0, points)! + compute.collision_point_mark_kernel.set_buffer_arg(0, slots.buffer()!.handle)! + compute.collision_point_mark_kernel.set_buffer_arg(1, compute.active_flags_a.buffer()!.handle)! compute.collision_point_mark_kernel.enqueue_1d(&compute.queue, usize(points.len), 0)! scan, active_count := compute.scan_flags(points.len)! if active_count == 0 { return 0, unsafe { active_points } } allocated_points := ensure_buffer_capacity(&compute.context, active_points, active_count)! - compute.collision_point_scatter_kernel.set_buffer_arg(0, slots.handle)! + mut keep := false + defer { + if isnil(active_points) && !keep { + mut pending := unsafe { allocated_points } + pending.close() or {} + } + } + compute.collision_point_scatter_kernel.set_buffer_arg(0, slots.buffer()!.handle)! compute.collision_point_scatter_kernel.set_buffer_arg(1, scan)! - compute.collision_point_scatter_kernel.set_buffer_arg(2, allocated_points.handle)! + compute.collision_point_scatter_kernel.set_buffer_arg(2, allocated_points.buffer()!.handle)! compute.collision_point_scatter_kernel.enqueue_1d(&compute.queue, usize(points.len), 0)! + keep = true return active_count, allocated_points } @@ -1016,12 +1030,12 @@ kernel void step_enemies(global EnemyMotion *items) { compute.collision_candidate_buffer = ensure_buffer_capacity(&compute.context, compute.collision_candidate_buffer, pair_count)! compute.active_flags_a = ensure_buffer_capacity(&compute.context, compute.active_flags_a, pair_count)! target_count := i32(active_target_count) - compute.collision_mark_kernel.set_buffer_arg(0, compute.collision_shots.handle)! - compute.collision_mark_kernel.set_buffer_arg(1, compute.collision_targets.handle)! + compute.collision_mark_kernel.set_buffer_arg(0, compute.collision_shots.buffer()!.handle)! + compute.collision_mark_kernel.set_buffer_arg(1, compute.collision_targets.buffer()!.handle)! compute.collision_mark_kernel.set_arg(2, &target_count)! compute.collision_mark_kernel.set_arg(3, &depth_limit)! compute.collision_mark_kernel.set_arg(4, &angle_limit)! - compute.collision_mark_kernel.set_buffer_arg(5, compute.active_flags_a.handle)! + compute.collision_mark_kernel.set_buffer_arg(5, compute.active_flags_a.buffer()!.handle)! compute.collision_mark_kernel.enqueue_1d(&compute.queue, usize(pair_count), 0)! scan, candidate_count := compute.scan_flags(pair_count)! if candidate_count == 0 { @@ -1031,16 +1045,16 @@ kernel void step_enemies(global EnemyMotion *items) { tested_pairs: pair_count } } - compute.collision_scatter_kernel.set_buffer_arg(0, compute.collision_shots.handle)! - compute.collision_scatter_kernel.set_buffer_arg(1, compute.collision_targets.handle)! + compute.collision_scatter_kernel.set_buffer_arg(0, compute.collision_shots.buffer()!.handle)! + compute.collision_scatter_kernel.set_buffer_arg(1, compute.collision_targets.buffer()!.handle)! compute.collision_scatter_kernel.set_arg(2, &target_count)! compute.collision_scatter_kernel.set_arg(3, &depth_limit)! compute.collision_scatter_kernel.set_arg(4, &angle_limit)! compute.collision_scatter_kernel.set_buffer_arg(5, scan)! - compute.collision_scatter_kernel.set_buffer_arg(6, compute.collision_candidate_buffer.handle)! + compute.collision_scatter_kernel.set_buffer_arg(6, compute.collision_candidate_buffer.buffer()!.handle)! compute.collision_scatter_kernel.enqueue_1d(&compute.queue, usize(pair_count), 0)! mut packed_candidates := []OpenCLShotCollisionCandidate{len: candidate_count} - compute.collision_candidate_buffer.read(&compute.queue, 0, mut packed_candidates)! + compute.collision_candidate_buffer.buffer()!.read(&compute.queue, 0, mut packed_candidates)! mut candidates := []ShotCollisionCandidate{cap: candidate_count} for candidate in packed_candidates { candidates << ShotCollisionCandidate{ diff --git a/v.mod b/v.mod index 11c4bb2..5afcbde 100644 --- a/v.mod +++ b/v.mod @@ -6,7 +6,7 @@ Module { license: 'MIT' repo_url: 'https://github.com/antono2/torus_trooper' dependencies: [ - 'antono2.opencl@v1.0.2', + 'antono2.opencl@v1.1.0', 'antono2.vulkan@v3.2.2', 'antono2.vkmemalloc@v2.6.2', ]