Compare commits

..
Author SHA1 Message Date
Cursor Agentandcodegirl007 e702497996 Clarify Agentbox names and invariants
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-18 06:05:46 +00:00
Cursor Agentandcodegirl007 4d2fb4a733 Organize Agentbox runtime code
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-18 06:03:59 +00:00
Cursor Agentandcodegirl007 f2fee5d26b Explain Agentbox architecture in plain language
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-17 18:19:11 +00:00
Cursor Agentandcodegirl007 c4f8dbeaaf Verify deterministic movement from observations
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-17 16:49:37 +00:00
Cursor Agentandcodegirl007 3037dbc2dd Allow staged applications to execute from tmpfs
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-17 16:48:13 +00:00
Cursor Agentandcodegirl007 e9197798f9 Document Agentbox runtime and model workflow
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-17 16:47:22 +00:00
Cursor Agentandcodegirl007 94146fcae2 Add runtime abstraction and agent loop
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-17 16:47:22 +00:00
Cursor Agentandcodegirl007 54dc5658c6 Wait for window manager readiness
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-17 16:40:23 +00:00
Cursor Agentandcodegirl007 ad5a4b4114 Stream Phase 1 artifacts from container tmpfs
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-17 16:39:29 +00:00
Cursor Agentandcodegirl007 96dade334d Add headless Agentbox environment spike
Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
2026-08-17 16:36:55 +00:00
codegirl007 98f56833a0 Add set_sprite_clip_def for pre-resolved clip switches 2026-08-14 23:19:30 -07:00
codegirl007 3cc4797a72 Test draw_sprite MAX_SPRITES enqueue capacity 2026-08-14 23:19:07 -07:00
codegirl007 f295aedf04 Merge master into draw-sprite-max-sprites-guard.
Resolve batching_test.odin by keeping master tests and the MAX_SPRITES guard test.
2026-08-14 23:17:48 -07:00
codegirl007 ba0b25980c Speed group_texture_runs for two-texture batches 2026-08-14 23:13:29 -07:00
codegirl007 71c76f180e Add crowd_batch, crowd_overdraw, and clip_thrash stress examples 2026-08-14 23:12:58 -07:00
codegirl007 00ba400d15 Add crowd_batch, crowd_overdraw, and clip_thrash stress examples. 2026-08-14 23:08:03 -07:00
codegirl007 c97a17f85b Speed group_texture_runs for two-texture batches. 2026-08-14 23:08:00 -07:00
codegirl007 a24f547709 Add set_sprite_clip_def for pre-resolved clip switches. 2026-08-14 23:07:58 -07:00
codegirl007 9df98ef73d Merge pull request #31 from Codegirl-Games/test-sprite-clip-cache
Test sprite clip_def cache on set_sprite_clip
2026-08-14 22:44:52 -07:00
codegirl007 f65b0354df Test that set_sprite_clip caches clip_def on sprites.
Locks spawn/switch cache fill and that a bad clip name leaves the prior cache intact.
2026-08-14 22:43:42 -07:00
codegirl007 cc196afd2e Cache sprite clip defs to cut per-frame map lookups 2026-08-14 22:42:15 -07:00
codegirl007 8d9faaaee0 Drop verbose comments from the clip-cache change. 2026-08-14 22:40:42 -07:00
codegirl007 41fb0a3d5a Cache clip defs on sprites to avoid per-frame map lookups.
Crowd stress (128 sprites) showed Clip_Def string map_get as the top engine cost; cache on set_sprite_clip and use it in update/draw.
2026-08-14 22:32:28 -07:00
codegirl007 dcf5ea29b0 Texture batching with prepare_draw_batches test coverage 2026-08-14 22:20:08 -07:00
codegirl007 70784b0f1b Test DXIL shader runtime preference without MSL 2026-08-14 22:19:16 -07:00
codegirl007 fb63ee21f4 Test draw_sprite refuses to exceed MAX_SPRITES.
Locks the enqueue capacity guard so a full draw_list cannot grow past the vertex buffer budget.
2026-08-14 22:18:42 -07:00
codegirl007 deef204bd3 Test DXIL preference when MSL is unavailable.
Locks the MSL > DXIL > SPIRV shader runtime order so Windows paths cannot silently fall through to Vulkan.
2026-08-14 22:16:58 -07:00
codegirl007 25dd3aa920 Add texture batching with TDD coverage at enqueue and prepare seams.
Opt-in batch_group regrouping cuts texture binds; prepare_draw_batches and draw_list tests lock the contract so end_frame cannot silently drop regroup.
2026-08-14 22:15:08 -07:00
codegirl007 0d4cdbd8ca Optimize build with -o:speed 2026-08-09 14:28:21 -07:00
codegirl007 9727aa4d7a Merge branch 'master' into optimized-build 2026-08-09 14:16:11 -07:00
codegirl007 b8b05954b1 fixes 2026-08-09 14:13:13 -07:00
codegirl007 58a2d5267e optimize build for speed 2026-08-09 14:05:28 -07:00
67 changed files with 9626 additions and 1356 deletions
@@ -1,208 +0,0 @@
# Add configurable internal render scale to improve fill-bound FPS
## Summary
Complete-frame benchmarks show that the current sprite renderer is primarily
pixel/fill bound on Vulkan/Lavapipe, not CPU draw-preparation bound. Rendering
the world into a smaller offscreen color target and nearest-blitting it to the
native swapchain produced the largest measured FPS improvement.
This should be an opt-in render-quality setting with a native-resolution
fallback. For pixel art, a 50% scale is especially useful because it maps to an
exact 2× nearest-neighbor upscale.
## Evidence
Environment:
- Commit `dc66203259e92ce39291556f7b6c26c8dd999b84`
- Odin `dev-2026-05-nightly:ea5175d`
- `-debug -o:speed`
- SDL 3.4.12, Vulkan/Lavapipe, immediate present
- 800×600 swapchain
- Five 150-frame trials per invocation after 50 warm-up frames
- Order-balanced baseline/candidate invocations
- GPU idle wait included before stopping each trial timer
### Fill and overdraw scaling
The committed `perf-frame` harness produced:
| Workload | Median frame time | Median FPS |
| --- | ---: | ---: |
| 1 visible sprite | 0.592 ms | 1,690 |
| 24 visible sprites | 2.249 ms | 445 |
| 64 visible sprites | 4.017 ms | 249 |
| 128 visible sprites | 6.255 ms | 160 |
| 128 stacked sprites | 7.692 ms | 130 |
Stacking the same 128 sprites increased frame time by approximately 23%,
confirming that overdraw matters.
With one centered sprite, render-target scaling produced:
| Window size | Pixels | Median frame time |
| --- | ---: | ---: |
| 400×300 | 120,000 | 0.268 ms |
| 800×600 | 480,000 | 0.545 ms |
| 1600×1200 | 1,920,000 | 2.075 ms |
The near-linear increase at larger sizes is further evidence of a pixel-bound
workload.
### Internal render-scale prototype
The temporary candidate kept the physical swapchain at 800×600, rendered
sprites into a smaller `COLOR_TARGET | SAMPLER` texture, and used
`BlitGPUTexture` with nearest filtering to upscale into the swapchain.
Aggregate medians across ten trials per mode:
| Workload | Native | 75% scale | 50% scale |
| --- | ---: | ---: | ---: |
| 128 spread sprites | 6.082 ms / 164 FPS | 4.082 ms / 245 FPS | 3.082 ms / 325 FPS |
| 128 stacked sprites | 7.912 ms / 126 FPS | 5.116 ms / 195 FPS | 3.034 ms / 330 FPS |
Compared with native resolution:
- 75% reduced frame time by 3335% and increased FPS by 4955%.
- 50% reduced frame time by 4962% and increased FPS by 97161%.
A visual smoke test confirmed that the 50% path rendered the complete scene at
the correct orientation and 800×600 output size. It was visibly coarser, as
expected. A 75% scale at 800×600 does not produce an integer upscale and can
create uneven pixel sizing with nearest filtering.
## Reproduction harness
Native spread and stacked workloads:
```bash
make perf-frame \
PERF_FRAME_SCENARIO=0 \
PERF_FRAME_SPRITES=128 \
PERF_FRAME_WIDTH=800 \
PERF_FRAME_HEIGHT=600
make perf-frame \
PERF_FRAME_SCENARIO=3 \
PERF_FRAME_SPRITES=128 \
PERF_FRAME_WIDTH=800 \
PERF_FRAME_HEIGHT=600
```
Run the same commands on the candidate branch with internal render scale set to
75% and 50%. Keep every `PERF_FRAME_*` value unchanged between comparisons.
## Suggested fix
Add explicit logical/output and internal-render dimensions to `App`:
```odin
Render_Scale :: enum {
Native,
Three_Quarter,
Half,
}
App :: struct {
// Existing swapchain fields remain the logical/output dimensions.
swapchain_texture: ^sdl.GPUTexture,
swapchain_w: u32,
swapchain_h: u32,
render_scale: Render_Scale,
scene_texture: ^sdl.GPUTexture,
scene_w: u32,
scene_h: u32,
}
```
Create the offscreen target with the swapchain format and both usages required
by SDL's blit path:
```odin
app.scene_texture = sdl.CreateGPUTexture(
app.device,
{
type = .D2,
format = sdl.GetGPUSwapchainTextureFormat(app.device, app.window),
usage = {.COLOR_TARGET, .SAMPLER},
width = app.scene_w,
height = app.scene_h,
layer_count_or_depth = 1,
num_levels = 1,
sample_count = ._1,
},
)
```
Render the sprite pass into `scene_texture`. Continue using the logical
swapchain dimensions for camera and clip-space calculations so world layout
does not change with render scale:
```odin
color_info := sdl.GPUColorTargetInfo {
texture = app.scene_texture,
clear_color = app.clear_color,
load_op = .CLEAR,
store_op = .STORE,
cycle = true,
}
```
After ending the render pass, upscale into the full swapchain:
```odin
blit := sdl.GPUBlitInfo {
source = {
texture = app.scene_texture,
w = app.scene_w,
h = app.scene_h,
},
destination = {
texture = app.swapchain_texture,
w = app.swapchain_w,
h = app.swapchain_h,
},
load_op = .DONT_CARE,
filter = .NEAREST,
}
sdl.BlitGPUTexture(app.cmd, blit)
```
Use the existing direct-to-swapchain path at native scale to avoid an
unnecessary blit. Recreate the offscreen texture whenever the swapchain size,
format, or render-scale setting changes. If native-resolution UI or text is
added later, render it after the world blit in a separate swapchain pass.
## Rejected experiment: fragment alpha discard
Approximately 49.7% of pixels inside the baked frame rectangles have exact
alpha zero. A temporary fragment shader discarded those texels:
```glsl
vec4 texel = texture(u_tex, v_uv);
if (texel.a == 0.0) {
discard;
}
out_color = texel;
```
Despite the high transparent coverage, this regressed frame time by roughly
56% in both spread and stacked workloads. Do not add alpha discard without
contradictory hardware-GPU evidence.
## Acceptance criteria
- Native, 75%, and 50% internal render-scale settings are available.
- Native scale retains the current direct-to-swapchain path.
- The scene target is recreated safely on resize, format change, or scale
change and released during shutdown.
- Camera/world coordinates remain stable when render scale changes.
- Nearest filtering is used for pixel-art output.
- Add screenshot-based checks for output orientation, viewport coverage, and
stable sprite placement at every scale.
- Benchmark spread and stacked scenarios on at least one hardware GPU.
- Document the quality tradeoff and recommend integer upscale ratios for pixel
art.
@@ -1,218 +0,0 @@
# Add deterministic CPU and frame benchmarks for sprite rendering
## Summary
The current `make flame` workflow is valuable for finding call stacks, but it
does not provide a reproducible performance metric:
- `examples/crowd` draws only 24 sprites.
- `MAX_SPRITES` limits the renderer to 128 sprites.
- Recording starts an interactive application and asks the user to play for
1020 seconds before quitting.
- The two checked-in reports contain only 499 and 469 samples.
- CPU queue construction and GPU submission are combined in one profile.
- The profiler currently builds unoptimized code.
These limitations make it difficult to tell whether a change made
`draw_sprite` faster, changed driver behavior, or merely changed sampling noise.
## Evidence
A temporary deterministic benchmark exposed two very different results:
1. CPU-only `draw_sprite`, two million calls and seven trials:
- `-debug`: median 192.560 ns/draw
- `-debug -o:speed`: median 19.154 ns/draw
2. Full 128-sprite frames through SDL GPU on Lavapipe, 1,000 measured frames
and five trials:
- `-debug`: median 2.486 ms/frame
- `-debug -o:speed`: median 2.447 ms/frame
At the current cap, optimized CPU queue construction is approximately 3.2
microseconds for 128 sprites. The full software-rendered frame is around 2.45
milliseconds, so optimizing `draw_sprite` cannot materially improve that
specific end-to-end workload. A hardware GPU or a larger future sprite limit
may have a different balance.
This split also explains why percentages from the current unoptimized
flamegraphs overstate small helper functions.
### Follow-up enhancement benchmarks
Six proposed renderer changes were implemented temporarily and measured before
being discarded. Full-frame tests used:
- Odin `dev-2026-05-nightly:ea5175d`
- `-debug -o:speed`
- SDL 3.4.12 with Vulkan/Lavapipe
- 128 animated sprites using real baked toad metadata and textures
- Warm-up before measurement
- Paired baseline/change samples on the same device with alternating order
- Ten 400-frame samples per mode, except culling, which used seven 750-frame
samples per mode
Each row is a separate paired run, so absolute frame times should only be
compared within that row.
| Enhancement | Baseline median | Changed median | Result |
| --- | ---: | ---: | ---: |
| Viewport culling, all visible | 4.845 ms | 4.822 ms | 0.5% faster |
| Viewport culling, 50% offscreen | 2.856 ms | 2.878 ms | 0.8% slower |
| SDL transfer and vertex buffer cycling | 4.869 ms | 4.834 ms | 0.7% faster |
| Contiguous vertex queue and one upload-side copy | 4.793 ms | 4.809 ms | 0.3% slower |
| Four-vertex indexed quads | 4.745 ms | 4.830 ms | 1.8% slower |
| GPU instancing with 32-byte instance records | 5.527 ms | 5.467 ms | 1.1% faster |
| Texture sorting, including sort cost, 128 runs to 2 | 5.765 ms | 5.654 ms | 1.9% faster |
Interpretation:
- Viewport culling is neutral at the current cap. The GPU already clips
offscreen triangles, and the sprites remain in one batched draw.
- SDL buffer cycling is a small performance improvement and is also the
documented way to avoid overwriting resources still bound by prior frames.
- Repacking the CPU queue does not help at 128 sprites; extra dynamic-array
work offsets the saved small-copy loop.
- Indexed quads regress performance despite reducing dynamic vertex data.
- Instancing reduces per-sprite upload data from 96 to 32 bytes, but the 1.1%
gain does not justify a second pipeline and shader path at the current cap.
- Texture sorting has the largest full-frame gain, but unrestricted sorting can
change alpha compositing. It is only safe within compatible layer/order
groups.
The recommended order is:
1. Profile optimized builds and establish the deterministic benchmark.
2. Apply the clip-space math simplification documented in the related issue.
3. Enable SDL buffer cycling for correct cross-frame resource reuse.
4. Consider layer-aware texture grouping if a 1.9% workload-specific gain is
worth the ordering complexity.
5. Defer culling, queue repacking, indexed quads, and instancing until the
sprite limit or measured workload grows substantially.
These results are from a software Vulkan backend. Hardware drivers may have a
different balance, which is another reason to keep the benchmark reproducible
and report backend details.
## Suggested fix
Add a non-interactive benchmark target with two explicitly separate workloads.
The committed harnesses are:
```make
PERF_ODIN_FLAGS ?= -debug -o:speed
perf-draw:
odin run benchmarks/draw_sprite \
-collection:pkg=. \
$(PERF_ODIN_FLAGS) \
-define:PERF_ITERATIONS=$(PERF_DRAW_ITERATIONS)
perf-frame:
odin run benchmarks/sprite_frame \
-collection:pkg=. \
$(PERF_ODIN_FLAGS) \
-define:PERF_FRAMES=$(PERF_FRAME_FRAMES) \
-define:PERF_SCENARIO=$(PERF_FRAME_SCENARIO)
```
Run the standard workloads with:
```bash
# CPU-only draw preparation.
make perf-draw
# Complete frame: 128 visible sprites sharing one texture.
make perf-frame PERF_FRAME_SCENARIO=0
# Complete frame: every second sprite is fully offscreen.
make perf-frame PERF_FRAME_SCENARIO=1
# Complete frame: 128 sprites alternate between two texture objects.
make perf-frame PERF_FRAME_SCENARIO=2
```
Every invocation prints the Git commit, Odin version, compiler flags, workload
configuration, every trial, and the median. `perf-frame` waits for GPU idle
after warm-up and after each measured frame batch so outstanding work is
included.
### CPU queue benchmark
- Construct `App`, `Character_Data`, and `Sprite` with real baked metadata.
- Use safe fake non-null GPU handles; `draw_sprite` only checks/stores these.
- Preallocate the draw list.
- Clear the queue whenever it reaches `MAX_SPRITES`.
- Vary sprite position between calls so the compiler cannot hoist the work.
- Warm up before timing.
- Run at least one million calls and report nanoseconds per draw.
- Build with `-o:speed` by default.
The measured loop should clear the queue at its cap, vary input to prevent
compiler hoisting, and report time per draw:
```odin
PERF_ITERATIONS :: #config(PERF_ITERATIONS, 2_000_000)
start := sdl.GetTicksNS()
for i in 0 ..< PERF_ITERATIONS {
if len(app.draw_list) == eng.MAX_SPRITES {
clear(&app.draw_list)
}
sprite.position.x = f32(i & 1023)
eng.draw_sprite(&app, &sprite)
}
elapsed := sdl.GetTicksNS() - start
fmt.printfln(
"%.3f ns/draw",
f64(elapsed) / f64(PERF_ITERATIONS),
)
```
### Full-frame benchmark
- Use a real SDL GPU device and baked texture.
- Warm up before timing.
- Run a fixed number of frames without interactive input.
- Report milliseconds per frame and sprites per second.
- Record GPU backend, present mode, compiler version, compiler flags, and sprite
count.
Use a fixed frame count rather than an interactive quit time:
```odin
PERF_FRAMES :: #config(PERF_FRAMES, 1_000)
for _ in 0 ..< 100 {
draw_benchmark_frame(&app, sprites[:]) // warm-up
}
start := sdl.GetTicksNS()
for _ in 0 ..< PERF_FRAMES {
draw_benchmark_frame(&app, sprites[:])
}
elapsed := sdl.GetTicksNS() - start
fmt.printfln(
"%.3f ms/frame",
f64(elapsed) / f64(PERF_FRAMES) / 1_000_000.0,
)
```
The CPU benchmark should be available without a display or GPU. The full-frame
benchmark may remain opt-in where a suitable GPU backend is unavailable.
Do not add a strict CI regression threshold initially; hosted runner variance
will make a single threshold flaky. CI can still compile the benchmark and
verify that it completes.
## Acceptance criteria
- A Makefile target runs the optimized CPU benchmark non-interactively.
- Results include compiler flags, iteration count, median, and per-trial values.
- CPU queue time is reported separately from complete frame time.
- Sprite positions or frames vary during the measured loop.
- The draw list never silently exceeds `MAX_SPRITES`.
- The benchmark has documented commands for repeatable local comparison.
- `make check` and `make test` continue to pass.
@@ -1,116 +0,0 @@
# Add opt-in order-safe texture batching
## Summary
`end_frame` batches only consecutive sprites that use the same texture.
Alternating two textures therefore produces one sampler bind and draw call per
sprite even when some sprites could safely be regrouped.
Globally sorting transparent sprites by texture is not correct: overlapping
sprites may blend differently when submission order changes. Batching should
therefore be opt-in within explicit groups whose members are safe to reorder.
## Evidence
A temporary paired benchmark used:
- Odin `dev-2026-05-nightly:ea5175d` with `-debug -o:speed`
- SDL 3.4.12 and Vulkan/Lavapipe
- 128 animated sprites alternating between two equivalent textures
- Sorting cost included in the measured frame
- Ten order-alternated samples of 400 frames per mode
| Mode | Texture runs | Median frame time |
| --- | ---: | ---: |
| Submission order | 128 | 5.765 ms |
| Texture grouped | 2 | 5.654 ms |
Sorting and grouping improved median frame time by approximately **1.9%**.
Hardware drivers with higher draw-call overhead may show a different result.
## Reproduction harness
Run the committed alternating-texture workload on the baseline and candidate
commits:
```bash
make perf-frame \
PERF_FRAME_SCENARIO=2 \
PERF_ODIN_FLAGS="-debug -o:speed"
```
Keep all `PERF_FRAME_*` values unchanged. The baseline should produce one
texture run per sprite; the candidate should reduce runs only inside explicit
reorder-safe groups. Compare `median_ms_per_frame` and verify rendered output.
## Suggested fix
Add an explicit batch group to queued sprites. Group `0` keeps strict submission
order; nonzero groups may be reordered only when the caller guarantees that
their members are order-independent.
```odin
Queued_Sprite :: struct {
texture: ^sdl.GPUTexture,
verts: [SPRITE_VERT_COUNT]Vertex,
batch_group: u32, // 0 = strict order; nonzero = caller permits regrouping
}
```
Sort each contiguous, nonzero group by texture immediately before upload:
```odin
group_texture_runs :: proc(list: []Queued_Sprite) {
start := 0
for start < len(list) {
group := list[start].batch_group
if group == 0 {
start += 1
continue
}
end := start + 1
for end < len(list) && list[end].batch_group == group {
end += 1
}
// Stable insertion sort is sufficient while MAX_SPRITES is 128.
for i in start + 1 ..< end {
item := list[i]
j := i
for j > start {
if uintptr(list[j - 1].texture) <= uintptr(item.texture) {
break
}
list[j] = list[j - 1]
j -= 1
}
list[j] = item
}
start = end
}
}
```
Call it after all sprites are queued and before the transfer-buffer copy:
```odin
group_texture_runs(app.draw_list[:])
```
Expose batching through a separate API or explicit parameter so existing
`draw_sprite` calls remain strict-order by default:
```odin
draw_sprite_batched(&app, &sprite, batch_group = 1)
```
## Acceptance criteria
- Existing `draw_sprite` behavior preserves exact submission order.
- Reordering requires an explicit nonzero batch group.
- Sorting never moves a sprite across a strict-order entry or group boundary.
- Add tests for strict order, group boundaries, stable same-texture ordering,
and reduced texture-run count.
- Add a visual overlap test confirming default alpha compositing is unchanged.
- Benchmark sorting cost and draw-call reduction on a hardware GPU.
@@ -1,83 +0,0 @@
# Enable SDL GPU buffer cycling for per-frame sprite uploads
## Summary
`end_frame` overwrites the same transfer buffer and vertex buffer every frame,
but both SDL calls currently pass `cycle = false`:
```odin
sdl.MapGPUTransferBuffer(app.device, app.transfer_buffer, false)
sdl.UploadToGPUBuffer(copy_pass, src, dst, false)
```
SDL documents cycling as the mechanism that rotates to an unbound internal
resource when the previous frame still references the current one. Enabling it
avoids an unnecessary resource dependency and makes the overwrite pattern
explicitly safe.
## Evidence
A temporary paired benchmark used:
- Odin `dev-2026-05-nightly:ea5175d` with `-debug -o:speed`
- SDL 3.4.12 and Vulkan/Lavapipe
- 128 animated sprites
- Ten order-alternated samples of 400 frames per mode
| Mode | Median frame time |
| --- | ---: |
| Cycling disabled | 4.869 ms |
| Cycling enabled | 4.834 ms |
Cycling improved median frame time by approximately **0.7%**. This is a small
performance change, but it also follows SDL's documented resource-reuse model.
## Reproduction harness
Run the committed full-frame harness on the baseline commit and candidate
commit:
```bash
make perf-frame \
PERF_FRAME_SCENARIO=0 \
PERF_ODIN_FLAGS="-debug -o:speed"
```
Keep all `PERF_FRAME_*` values unchanged. Compare `median_ms_per_frame`; the
harness waits for GPU idle before stopping each trial timer.
## Suggested fix
Cycle both resources that are fully overwritten each frame:
```odin
map_ptr := sdl.MapGPUTransferBuffer(
app.device,
app.transfer_buffer,
true, // rotate if the previous frame still binds this transfer buffer
)
// Write the complete [0, n * SPRITE_VERTS_SIZE) range, then unmap.
sdl.UnmapGPUTransferBuffer(app.device, app.transfer_buffer)
copy_pass := sdl.BeginGPUCopyPass(cmd)
sdl.UploadToGPUBuffer(
copy_pass,
src,
dst,
true, // rotate the destination vertex buffer if it is still bound
)
sdl.EndGPUCopyPass(copy_pass)
```
Cycling makes previous contents undefined, so this remains correct only because
the renderer writes the complete vertex range used by the frame before drawing.
Do not enable cycling for partial updates that depend on untouched data.
## Acceptance criteria
- Both `MapGPUTransferBuffer` and `UploadToGPUBuffer` use `cycle = true`.
- The complete submitted vertex range is rewritten every frame.
- Existing engine tests and examples continue to pass.
- A full-frame benchmark confirms no regression on a hardware GPU backend.
- Add a comment explaining why cycling is safe for this full-overwrite path.
@@ -1,98 +0,0 @@
# Profile optimized sprite builds instead of unoptimized debug code
## Summary
`make flame-build` currently compiles the selected example with `-debug` but
without an optimization mode:
```make
odin build examples/$(FLAME_EXAMPLE) -collection:pkg=. -out:$(FLAME_BIN) -debug
```
This makes the flamegraph useful for debugging but misleading for performance
decisions. The current profiles largely describe code that will disappear or
be inlined in an optimized build.
## Evidence
The two checked-in `crowd` profiles were captured from this unoptimized binary.
They report:
- `engine::draw_sprite`: 9.56% and 12.22% self time
- `engine::to_clip`: 5.33% and 5.51% self time
- `engine::sprite_feet_quad`: 3.98% and 3.30% self time
- Additional time in string hashing/map lookup, bounds checks, and dynamic
array append helpers
A CPU-only benchmark using the real `draw_sprite`, real baked toad metadata,
preallocated draw list, changing sprite positions, and two million draws per
trial produced:
| Build | Median time per draw | Trials |
| --- | ---: | ---: |
| `-debug` (current Makefile behavior) | 192.560 ns | 7 |
| `-debug -o:speed` | 19.154 ns | 7 |
The optimized build is about **10.1x faster** without an engine code change.
An end-to-end 128-sprite benchmark on SDL 3.4.12 with Lavapipe showed only a
small full-frame difference (median 2.486 ms debug versus 2.447 ms optimized)
because software GPU/driver work dominated. This does not invalidate the CPU
result; it shows why CPU queue time and full-frame time must be reported
separately.
Environment:
- Odin `dev-2026-05-nightly:ea5175d` (the version pinned by CI)
- SDL 3.4.12
- Linux x86-64
## Suggested fix
Compile profiling binaries with optimization while retaining symbols:
```make
FLAME_ODIN_FLAGS ?= -debug -o:speed
flame-build:
odin build examples/$(FLAME_EXAMPLE) \
-collection:pkg=. \
-out:$(FLAME_BIN) \
$(FLAME_ODIN_FLAGS)
```
Keeping the flags configurable allows an explicitly unoptimized diagnostic run
without making it the performance default.
Example usage:
```bash
# Representative performance profile: optimized code with debug symbols.
make flame FLAME_EXAMPLE=crowd
# Explicitly profile unoptimized code when investigating debug-only behavior.
make flame FLAME_EXAMPLE=crowd FLAME_ODIN_FLAGS="-debug -o:none"
```
Reproduce the isolated build-mode comparison with the committed CPU harness:
```bash
make perf-draw PERF_ODIN_FLAGS="-debug -o:none"
make perf-draw PERF_ODIN_FLAGS="-debug -o:speed"
```
Both runs print the Git commit, Odin version, compiler flags, every trial, and
the median nanoseconds per draw.
Consider applying an explicit optimization mode to performance-oriented example
runs as well. Plain `odin run` currently uses Odin's unoptimized default.
## Acceptance criteria
- `make flame-build` produces an optimized binary with debug symbols.
- `FLAME_ODIN_FLAGS` can override the default for diagnostic builds.
- `make check` and `make test` continue to pass.
- A new `crowd` profile records the exact compiler flags in its report or
accompanying documentation.
- Performance conclusions distinguish CPU `draw_sprite` cost from complete
frame/GPU submission cost.
@@ -1,139 +0,0 @@
# Prototype instanced sprite rendering for larger batches
## Summary
The renderer currently generates and uploads six complete vertices per sprite:
```text
6 vertices × (position float2 + UV float2) = 96 bytes/sprite/frame
```
Instancing can keep one immutable six-corner unit quad on the GPU and upload one
rectangle/UV record per sprite:
```text
clip rectangle float4 + UV rectangle float4 = 32 bytes/sprite/frame
```
This reduces dynamic upload volume by two thirds, but requires another pipeline
and backend-specific vertex shader. At the current 128-sprite cap, the measured
gain is too small to justify enabling it unconditionally.
## Evidence
A complete temporary instanced path was implemented with:
- A static six-corner vertex buffer
- A 32-byte per-instance buffer
- Vertex-rate and instance-rate pipeline inputs
- One instanced draw per texture run
- Validated SPIR-V and SDL's debug GPU device
The paired benchmark used ten order-alternated samples of 400 frames:
| Mode | Median frame time |
| --- | ---: |
| Six dynamic vertices per sprite | 5.527 ms |
| 32-byte instance per sprite | 5.467 ms |
Instancing improved median frame time by approximately **1.1%** on
Vulkan/Lavapipe with 128 sprites.
## Reproduction harness
Run the committed visible full-frame workload on baseline and candidate commits:
```bash
make perf-frame \
PERF_FRAME_SCENARIO=0 \
PERF_FRAME_SPRITES=128 \
PERF_ODIN_FLAGS="-debug -o:speed"
```
Keep all other `PERF_FRAME_*` values unchanged and compare
`median_ms_per_frame`. To test 512 or more sprites, first raise the engine's
`MAX_SPRITES` and associated buffer capacities on the candidate branch, then
set `PERF_FRAME_SPRITES` to the same value.
## Suggested fix
Treat this as a prototype gated by larger sprite counts or a demonstrated
hardware bottleneck, not as an immediate replacement.
Define the compact instance payload:
```odin
Sprite_Instance :: struct {
clip_rect: [4]f32, // left, top, right, bottom
uv_rect: [4]f32, // u0, v0, u1, v1
}
```
Queue one record after the existing quad and UV calculations:
```odin
instance := Sprite_Instance {
clip_rect = {p0.x, p0.y, p2.x, p2.y},
uv_rect = {u0, v0, u1, v1},
}
append(&app.instance_list, instance)
```
Use a static unit quad:
```odin
UNIT_QUAD := [6]Vec2 {
{0, 0}, {1, 0}, {1, 1},
{0, 0}, {1, 1}, {0, 1},
}
```
The instanced vertex shader reconstructs position and UV:
```glsl
#version 450
layout(location = 0) in vec2 in_corner;
layout(location = 1) in vec4 in_clip_rect;
layout(location = 2) in vec4 in_uv_rect;
layout(location = 0) out vec2 v_uv;
void main() {
vec2 position = mix(in_clip_rect.xy, in_clip_rect.zw, in_corner);
v_uv = mix(in_uv_rect.xy, in_uv_rect.zw, in_corner);
gl_Position = vec4(position, 0.0, 1.0);
}
```
Configure slot 0 as vertex-rate and slot 1 as instance-rate, then draw each
texture run with six vertices and `run` instances:
```odin
vb_descs := [2]sdl.GPUVertexBufferDescription {
{slot = 0, pitch = u32(size_of(Vec2)), input_rate = .VERTEX},
{slot = 1, pitch = u32(size_of(Sprite_Instance)), input_rate = .INSTANCE},
}
sdl.DrawGPUPrimitives(
app.render_pass,
6, // unit-quad vertices
u32(run), // sprite instances in this texture run
0,
0,
)
```
Keep the current path as a fallback until the instanced implementation exists
for Vulkan, D3D12, and Metal and demonstrates a meaningful hardware win.
## Acceptance criteria
- Benchmark at 128, 512, 2,048, and 10,000 sprites or the highest supported
counts.
- Report CPU queue time, bytes uploaded, and complete frame time separately.
- Require a meaningful hardware improvement before changing the default path.
- Supply equivalent Vulkan, D3D12, and Metal shaders.
- Preserve texture-run batching and sprite flip/trim behavior.
- Add visual equivalence tests for position, UVs, animation frames, and flip.
- Retain the existing six-vertex path as a fallback during evaluation.
@@ -1,130 +0,0 @@
# Reduce repeated clip-space work in `draw_sprite`
## Summary
`draw_sprite` calls `to_clip` four times for an axis-aligned quad:
```odin
p0 := to_clip(x0_px, y0_px, sw, sh)
p1 := to_clip(x1_px, y0_px, sw, sh)
p2 := to_clip(x1_px, y1_px, sw, sh)
p3 := to_clip(x0_px, y1_px, sw, sh)
```
This repeats the same divisions and converts duplicate x/y coordinates. An
axis-aligned sprite has only two unique x values and two unique y values.
This is a measurable optimization, but it is low priority at the current
128-sprite limit because the absolute saving is small.
## Evidence
The checked-in unoptimized profiles report `engine::to_clip` at 5.33% and 5.51%
self time. Those percentages are inflated by the unoptimized profiling build,
so the change was also measured with `-debug -o:speed`.
A temporary benchmark used the real baked toad metadata, changed sprite
position on every iteration, preallocated the queue, and performed two million
draws per mode over seven trials:
| Mode | Median time per draw |
| --- | ---: |
| Current `draw_sprite` | 25.128 ns |
| Precomputed clip scale and reused coordinates | 21.531 ns |
| Same math plus cached `Frame_Def` | 21.778 ns |
Simplifying the math improved isolated draw time by approximately **14.3%**.
Caching the resolved frame did not provide an additional benefit and should not
be included without new evidence.
At `MAX_SPRITES == 128`, the measured math saving is only about 0.46
microseconds per completely full frame. GPU/driver work dominated the
end-to-end benchmark, so this should follow the profiling and deterministic
benchmark improvements.
Follow-up paired full-frame experiments at the 128-sprite cap found no larger
renderer-architecture win: buffer cycling improved median frame time by 0.7%,
GPU instancing by 1.1%, and texture sorting by 1.9%, while culling, queue
repacking, and indexed quads were neutral or slower. The clip-space change
therefore remains the strongest measured optimization specifically inside
`draw_sprite`, although its absolute frame impact is still small.
Environment:
- Odin `dev-2026-05-nightly:ea5175d`
- Optimized with `-debug -o:speed`
- Linux x86-64
## Reproduction harness
Run the committed CPU harness on the baseline commit and again after applying
the suggested fix:
```bash
make perf-draw PERF_ODIN_FLAGS="-debug -o:speed"
```
Keep `PERF_DRAW_ITERATIONS`, `PERF_DRAW_WARMUP`, and `PERF_DRAW_TRIALS`
unchanged between commits. Compare `median_ns_per_draw`.
## Suggested fix
Compute clip scaling once and construct corners from the unique coordinates:
```odin
sprite_quad_to_clip :: proc(x0, y0, x1, y1, sw, sh: f32) -> [4]Vec2 {
sx := 2.0 / sw
sy := 2.0 / sh
left := x0 * sx - 1
right := x1 * sx - 1
top := 1 - y0 * sy
bottom := 1 - y1 * sy
return {
{left, top},
{right, top},
{right, bottom},
{left, bottom},
}
}
points := sprite_quad_to_clip(x0_px, y0_px, x1_px, y1_px, sw, sh)
p0, p1, p2, p3 := points[0], points[1], points[2], points[3]
```
Keep `to_clip` for general callers and its existing tests; this change only
specializes quad construction inside `draw_sprite`.
Add an equivalence test before replacing the current calls:
```odin
@(test)
sprite_quad_clip_math_matches_to_clip :: proc(t: ^testing.T) {
x0, y0 := f32(125), f32(80)
x1, y1 := f32(325), f32(280)
sw, sh := f32(800), f32(600)
expected := [4]Vec2 {
to_clip(x0, y0, sw, sh),
to_clip(x1, y0, sw, sh),
to_clip(x1, y1, sw, sh),
to_clip(x0, y1, sw, sh),
}
actual := sprite_quad_to_clip(x0, y0, x1, y1, sw, sh)
for i in 0 ..< 4 {
testing.expect_value(t, actual[i], expected[i])
}
}
```
## Acceptance criteria
- Existing sprite geometry, camera, UV, and batching tests pass.
- Add or extend a test that compares all four generated corners against
`to_clip` for representative viewport and sprite coordinates.
- Flipped and unflipped sprites produce identical vertices to the current code.
- An optimized deterministic benchmark shows at least a 10% improvement in
isolated `draw_sprite` time under comparable conditions.
- Do not add a per-sprite frame cache as part of this issue.
+19 -42
View File
@@ -1,8 +1,7 @@
.PHONY: shaders-vulkan shaders-d3d12 shaders-metal shaders-all bake toad hello_sprite crowd camera_sandbox clips check test help
.PHONY: shaders-vulkan shaders-d3d12 shaders-metal shaders-all bake toad hello_sprite crowd crowd_batch crowd_overdraw clip_thrash camera_sandbox clips check test help
.PHONY: flame flame-build flame-record flame-svg flame-report flame-tools
.PHONY: perf-draw perf-frame
# Flamegraph profiling (needs: pacman -S perf). Example: make flame or make flame FLAME_EXAMPLE=toad
# Flamegraph profiling (needs: pacman -S perf). Example: make flame or make flame FLAME_EXAMPLE=crowd_batch
FLAME_EXAMPLE ?= crowd
FLAME_BIN := $(FLAME_EXAMPLE)_perf
FLAMEGRAPH_DIR ?= tools/FlameGraph
@@ -13,19 +12,6 @@ FLAME_STAMP := $(shell date +%Y%m%d-%H%M%S)
endif
FLAME_PREFIX := $(FLAME_OUT_DIR)/$(FLAME_EXAMPLE)-$(FLAME_STAMP)
PERF_DRAW_ITERATIONS ?= 2000000
PERF_DRAW_WARMUP ?= 10000
PERF_DRAW_TRIALS ?= 7
PERF_ODIN_FLAGS ?= -debug -o:speed
PERF_FRAME_SPRITES ?= 128
PERF_FRAME_FRAMES ?= 400
PERF_FRAME_WARMUP ?= 100
PERF_FRAME_TRIALS ?= 10
# 0=visible, 1=half offscreen, 2=alternating textures, 3=stacked
PERF_FRAME_SCENARIO ?= 0
PERF_FRAME_WIDTH ?= 800
PERF_FRAME_HEIGHT ?= 600
help:
@echo "Targets:"
@echo " bake Bake all characters under content/characters/"
@@ -38,15 +24,16 @@ help:
@echo " toad Run the toad example (full demo)"
@echo " hello_sprite Minimal load + draw"
@echo " crowd Many sprites, one Character_Data"
@echo " crowd_batch Multi-texture batching stress (two toad loads)"
@echo " crowd_overdraw Stacked sprites overdraw stress"
@echo " clip_thrash Per-frame idle/walk clip flip stress"
@echo " camera_sandbox Pan camera / Space toggles follow"
@echo " clips Keys 1/2 switch idle/walk"
@echo " flame Build+record+SVG+JPG+text report (FLAME_EXAMPLE=$(FLAME_EXAMPLE))"
@echo " flame-build Debug binary only ($(FLAME_BIN))"
@echo " flame-build Optimized debug binary ($(FLAME_BIN))"
@echo " flame-record perf record (play, then quit)"
@echo " flame-svg Convert perf.data -> $(FLAME_PREFIX).svg/.jpg"
@echo " flame-report Convert perf.data -> $(FLAME_PREFIX)-report.txt"
@echo " perf-draw Deterministic CPU draw benchmark"
@echo " perf-frame Deterministic SDL GPU frame benchmark (scenario 0/1/2/3)"
bake:
./scripts/bake_all.sh
@@ -70,31 +57,12 @@ check:
odin check examples/toad -collection:pkg=.
odin check examples/hello_sprite -collection:pkg=.
odin check examples/crowd -collection:pkg=.
odin check examples/crowd_batch -collection:pkg=.
odin check examples/crowd_overdraw -collection:pkg=.
odin check examples/clip_thrash -collection:pkg=.
odin check examples/camera_sandbox -collection:pkg=.
odin check examples/clips -collection:pkg=.
perf-draw:
@echo "git_commit=$$(git rev-parse HEAD)"
@echo "odin_version=$$(odin version)"
@echo "odin_flags=$(PERF_ODIN_FLAGS)"
odin run benchmarks/draw_sprite -collection:pkg=. $(PERF_ODIN_FLAGS) \
-define:PERF_ITERATIONS=$(PERF_DRAW_ITERATIONS) \
-define:PERF_WARMUP=$(PERF_DRAW_WARMUP) \
-define:PERF_TRIALS=$(PERF_DRAW_TRIALS)
perf-frame:
@echo "git_commit=$$(git rev-parse HEAD)"
@echo "odin_version=$$(odin version)"
@echo "odin_flags=$(PERF_ODIN_FLAGS)"
odin run benchmarks/sprite_frame -collection:pkg=. $(PERF_ODIN_FLAGS) \
-define:PERF_SPRITES=$(PERF_FRAME_SPRITES) \
-define:PERF_FRAMES=$(PERF_FRAME_FRAMES) \
-define:PERF_WARMUP_FRAMES=$(PERF_FRAME_WARMUP) \
-define:PERF_TRIALS=$(PERF_FRAME_TRIALS) \
-define:PERF_SCENARIO=$(PERF_FRAME_SCENARIO) \
-define:PERF_WIDTH=$(PERF_FRAME_WIDTH) \
-define:PERF_HEIGHT=$(PERF_FRAME_HEIGHT)
toad:
odin run examples/toad -collection:pkg=.
@@ -104,6 +72,15 @@ hello_sprite:
crowd:
odin run examples/crowd -collection:pkg=.
crowd_batch:
odin run examples/crowd_batch -collection:pkg=.
crowd_overdraw:
odin run examples/crowd_overdraw -collection:pkg=.
clip_thrash:
odin run examples/clip_thrash -collection:pkg=.
camera_sandbox:
odin run examples/camera_sandbox -collection:pkg=.
@@ -117,7 +94,7 @@ flame-tools:
fi
flame-build:
odin build examples/$(FLAME_EXAMPLE) -collection:pkg=. -out:$(FLAME_BIN) -debug
odin build examples/$(FLAME_EXAMPLE) -collection:pkg=. -out:$(FLAME_BIN) -debug -o:speed
flame-record: flame-build
@echo ">>> Profiling ./$(FLAME_BIN) — play for ~1020s under load, then quit the window."
+3
View File
@@ -0,0 +1,3 @@
.agentbox/
/agentbox
examples/mover/mover
+16
View File
@@ -0,0 +1,16 @@
.PHONY: build demo demo-binary phase1 test
build:
go build -o agentbox ./cmd/agentbox
demo-binary:
./scripts/build_demo.sh
demo: build demo-binary
./agentbox run ./examples/mover --task "Launch the application, move the character to the right, and describe what happened."
phase1:
go run ./cmd/agentbox phase1
test:
go test ./...
+129
View File
@@ -0,0 +1,129 @@
# Agentbox
This is a local technical spike for a programmable graphical Linux
environment. It proves that a controller can launch an ordinary GUI process
headlessly, observe pixels and logs, inject keyboard and mouse input, drive the
application through a replaceable agent, record a trace, and shut the
environment down.
The default agent is deterministic and requires no API credentials. One
optional OpenAI Responses adapter demonstrates screenshot + text model control.
This is still a local spike, not a cloud control plane or production sandbox.
See [the architecture decision](docs/architecture.md) for the choices and
security boundary.
## Requirements
- Linux
- Go 1.22 or newer
- Docker Engine with a running daemon
- permission to use Docker without an interactive `sudo` prompt
No X server, window manager, or C compiler is required on the host; those
dependencies are built into the container image.
## One-command demo
From this directory:
```sh
make demo
```
That one command:
1. builds the `agentbox` CLI and mover executable;
2. creates a restricted Xvfb/Openbox environment;
3. stages and launches the executable;
4. runs the deterministic observe/press/wait/release/observe loop;
5. records screenshots, actions, decisions, logs, and final status;
6. stops and removes the environment, including on failure or interruption.
The equivalent explicit commands are:
```sh
make build demo-binary
./agentbox run ./examples/mover \
--task "Launch the application, move the character to the right, and describe what happened."
```
Flags can appear before or after the path.
## Run a private executable
Pass a prebuilt Linux executable:
```sh
./agentbox run ./path/to/application --task "Open the menu and click Settings."
```
For a directory, add `agentbox.json`:
```json
{
"command": "game",
"args": ["--windowed"],
"env": {"EXAMPLE": "value"},
"window_title": "My Game"
}
```
The command is resolved relative to the manifest. The current runtime is
Debian-based, so dynamically linked executables must have compatible libraries.
## Model agent
The model adapter is opt-in:
```sh
export OPENAI_API_KEY="..."
./agentbox run ./path/to/application \
--task "Move the character right and report the result." \
--agent openai \
--model gpt-5
```
Each request contains the task, current screenshot, previous actions, step
history, and recent application logs. Strict structured output allows one
backend-neutral input action or completion. Use `--max-steps` to bound a run.
The environment itself still has no network access.
## Runs and inspection
```sh
./agentbox runs
./agentbox inspect <run-id>
```
Each run writes:
```text
.agentbox/runs/<run-id>/
├── run.json
├── actions.jsonl
├── steps.jsonl
├── screenshots/
│ ├── 0001.png
│ └── ...
├── stdout.log
└── stderr.log
```
`run.json` has `schema_version: "1"` plus task, application, agent, timestamps,
status, and step count. `steps.jsonl` records each observation reference, agent
message, and action. `actions.jsonl` is a compact action-only stream.
## Original environment proof
The visual centroid test from Phase 1 remains available:
```sh
make phase1
```
## Development checks
```sh
make test
go vet ./...
```
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"path/filepath"
"agentbox/internal/trace"
)
func listRuns() error {
projectRoot, err := findProjectRoot()
if err != nil {
return err
}
runs, err := trace.List(projectRoot)
if err != nil {
return err
}
if len(runs) == 0 {
fmt.Println("No recorded runs.")
return nil
}
for _, run := range runs {
fmt.Printf("%s %-8s %-20s %s\n",
run.ID, run.Status, run.Agent, run.Task)
}
return nil
}
func inspect(args []string) error {
if len(args) != 1 {
return errors.New("inspect requires one run ID")
}
projectRoot, err := findProjectRoot()
if err != nil {
return err
}
runID := args[0]
run, err := trace.Read(projectRoot, runID)
if err != nil {
return err
}
steps, err := trace.ReadSteps(projectRoot, runID)
if err != nil {
return err
}
output := struct {
Run trace.Run `json:"run"`
Steps []trace.StepRecord `json:"steps"`
Directory string `json:"directory"`
}{
Run: run,
Steps: steps,
Directory: filepath.Join(projectRoot, ".agentbox", "runs", run.ID),
}
data, err := json.MarshalIndent(output, "", " ")
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"agentbox/internal/phase1"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
var err error
switch os.Args[1] {
case "phase1":
err = phase1.Run(ctx)
case "run":
err = run(ctx, os.Args[2:])
case "runs":
err = listRuns()
case "inspect":
err = inspect(os.Args[2:])
default:
usage()
os.Exit(2)
}
if err != nil {
fmt.Fprintf(os.Stderr, "agentbox: %v\n", err)
os.Exit(1)
}
}
func usage() {
fmt.Fprintln(os.Stderr, `usage:
agentbox run <path> --task "<task>" [--agent deterministic|openai] [--model <model>]
agentbox runs
agentbox inspect <run-id>
agentbox phase1`)
}
+17
View File
@@ -0,0 +1,17 @@
package main
import "testing"
func TestParseRunOptionsAllowsFlagsAfterPath(t *testing.T) {
options, err := parseRunOptions([]string{
"./game", "--task", "move right", "--max-steps=7",
})
if err != nil {
t.Fatal(err)
}
if options.applicationPath != "./game" ||
options.task != "move right" ||
options.maxSteps != 7 {
t.Fatalf("options = %#v", options)
}
}
+28
View File
@@ -0,0 +1,28 @@
package main
import (
"bytes"
"errors"
"os"
"path/filepath"
)
// findProjectRoot lets commands work from any directory inside the Agentbox
// module. The module declaration is a stronger marker than a directory name.
func findProjectRoot() (string, error) {
currentDirectory, err := os.Getwd()
if err != nil {
return "", err
}
for {
goModule, readErr := os.ReadFile(filepath.Join(currentDirectory, "go.mod"))
if readErr == nil && bytes.Contains(goModule, []byte("module agentbox")) {
return currentDirectory, nil
}
parentDirectory := filepath.Dir(currentDirectory)
if parentDirectory == currentDirectory {
return "", errors.New("run agentbox from inside its module directory")
}
currentDirectory = parentDirectory
}
}
+141
View File
@@ -0,0 +1,141 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
"agentbox/internal/agent"
"agentbox/internal/appspec"
"agentbox/internal/environment/dockerx11"
agentRuntime "agentbox/internal/runtime"
"agentbox/internal/trace"
)
type runOptions struct {
applicationPath string
task string
agentName string
modelName string
maxSteps int
}
func run(ctx context.Context, args []string) error {
options, err := parseRunOptions(args)
if err != nil {
return err
}
projectRoot, err := findProjectRoot()
if err != nil {
return err
}
applicationCommand, err := appspec.Resolve(options.applicationPath)
if err != nil {
return err
}
controller, err := newAgent(options)
if err != nil {
return err
}
traceStore, err := trace.New(
projectRoot,
options.task,
options.applicationPath,
controller.Name(),
)
if err != nil {
return err
}
desktopEnvironment := dockerx11.New(dockerx11.Config{
ProjectRoot: projectRoot,
RunID: traceStore.ID(),
Output: os.Stdout,
})
err = agentRuntime.Run(ctx, agentRuntime.Config{
Task: options.task,
Command: applicationCommand,
Controller: controller,
Environment: desktopEnvironment,
TraceStore: traceStore,
MaxSteps: options.maxSteps,
Output: os.Stdout,
})
if err != nil {
fmt.Fprintf(os.Stderr, "Run artifacts: %s\n", traceStore.Directory())
return err
}
fmt.Printf("Run artifacts written to:\n%s\n", traceStore.Directory())
return nil
}
// parseRunOptions accepts flags before or after the application path so the CLI
// matches the natural "agentbox run ./app --task ..." form shown in the docs.
func parseRunOptions(args []string) (runOptions, error) {
options := runOptions{
agentName: "deterministic",
modelName: "gpt-5",
maxSteps: 20,
}
for index := 0; index < len(args); index++ {
argument := args[index]
if !strings.HasPrefix(argument, "--") {
if options.applicationPath != "" {
return options, errors.New("run accepts exactly one application path")
}
options.applicationPath = argument
continue
}
flagName, flagValue, _ := strings.Cut(strings.TrimPrefix(argument, "--"), "=")
if flagValue == "" {
index++
if index >= len(args) {
return options, fmt.Errorf("--%s requires a value", flagName)
}
flagValue = args[index]
}
switch flagName {
case "task":
options.task = flagValue
case "agent":
options.agentName = flagValue
case "model":
options.modelName = flagValue
case "max-steps":
maxSteps, err := strconv.Atoi(flagValue)
if err != nil || maxSteps < 1 {
return options, errors.New("--max-steps must be a positive integer")
}
options.maxSteps = maxSteps
default:
return options, fmt.Errorf("unknown flag --%s", flagName)
}
}
if options.applicationPath == "" {
return options, errors.New("run requires an application path")
}
if options.task == "" {
return options, errors.New("run requires --task")
}
return options, nil
}
func newAgent(options runOptions) (agent.Agent, error) {
switch options.agentName {
case "deterministic":
return &agent.Deterministic{}, nil
case "openai":
return agent.NewOpenAI(agent.OpenAIConfig{
APIKey: os.Getenv("OPENAI_API_KEY"),
Model: options.modelName,
})
default:
return nil, fmt.Errorf(
"unknown agent %q (want deterministic or openai)",
options.agentName,
)
}
}
+290
View File
@@ -0,0 +1,290 @@
# How Agentbox works
This document explains the prototype in simple terms. The exact technical names
are included in parentheses for readers who want to dig deeper.
## The big idea
Imagine giving a robot its own computer in a locked room.
The robot cannot look inside the program or use secret game controls. It can
only:
- look at the screen;
- press and release keyboard keys;
- move and click the mouse;
- read messages printed by the program.
Agentbox builds that room, starts the program, lets the robot interact with it,
and records everything that happened.
## The main parts
Each part has one job:
```text
You type a command
|
v
CLI: understands what you asked for
|
v
Runtime: runs the experiment step by step
/ \
v v
Environment Agent
"the computer" "the robot"
| |
+------v-------+
|
Trace
"the experiment notebook"
```
- **CLI:** The `agentbox` command you run in a terminal.
- **Runtime:** The referee. It asks the environment for a screenshot, gives it
to the agent, carries out the agent's next action, and repeats.
- **Environment:** The temporary Linux computer containing the application.
- **Application:** The game or other interactive program being tested.
- **Agent:** The decision-maker. It can be a fixed test script or an AI model.
- **Trace:** The saved screenshots, actions, logs, and final result.
The important rule is that these parts do not cheat by reaching into each
other. The agent does not know about Docker or X11. The environment does not
know whether the agent is OpenAI, another model, or a fixed script.
## What happens during one run
When you run:
```sh
agentbox run ./my-game --task "Move the character right."
```
Agentbox does this:
1. Creates a fresh temporary room.
2. Copies the executable into that room.
3. Starts a pretend monitor inside the room.
4. Launches the application on that monitor.
5. Takes a screenshot.
6. Gives the screenshot, task, recent logs, and earlier actions to the agent.
7. Receives one action, such as `key_down RIGHT`.
8. Sends that action to the temporary computer.
9. Repeats until the agent says it is finished or reaches the step limit.
10. Saves the experiment and destroys the temporary room.
Stopping on errors is important. Even if the application or agent fails,
Agentbox still tries to save the logs and remove the environment.
## How the pretend computer works
The temporary room is a **Docker container**. A container is like a lightweight
box around a group of programs. It keeps files and processes separate enough
for this experiment, but it is not strong enough to safely hold a determined
attacker. The security section explains that limitation.
There is no physical monitor in the container, so we use a pretend one:
- **Xvfb** is a screen that exists only in memory. The application thinks it is
drawing to a normal 640×480 monitor.
- **Openbox** acts like a tiny desktop. It puts windows in the right place and
decides which window receives keyboard input.
- **scrot** takes a picture of the pretend monitor and saves it as a PNG.
- **xdotool** sends ordinary-looking keyboard and mouse events.
The application is not modified to accept special Agentbox commands. From its
point of view, a person pressed a key or clicked the mouse.
## Why use these old-looking tools?
### Why X11 instead of Wayland?
Linux has two common ways to manage graphical windows: X11 and Wayland.
Wayland is newer and safer for everyday desktops. One of its safety features is
that programs cannot freely spy on the whole screen or pretend to be the
keyboard. Those are exactly the powers Agentbox needs, so a headless Wayland
version would need more complicated, compositor-specific plumbing.
X11 already has small, well-understood tools for this experiment. Because all
X11 details are hidden behind the `Environment` interface, we can replace this
backend later without changing the agent.
One X11 screen is used per environment. Programs sharing an X11 screen can
interfere with each other, so unrelated runs must never share one.
### Why no VNC or browser viewer?
VNC would let a human watch the desktop live. That sounds useful, but the agent
only needs screenshots and input for now. Adding VNC would mean more servers,
network ports, and video encoding without proving anything new. It can be added
later as a viewer without changing how the agent thinks.
### What about a graphics card?
This version draws with the CPU instead of a GPU. That is enough for the simple
demo and many desktop applications. A demanding 3D game may need a future
environment that supplies a virtual or real GPU.
### Why is the demo written with Xlib?
The demo uses a tiny C/Xlib program because it has very few dependencies. It
proves the screen and input path directly. Agentbox is not tied to Xlib: a
staged application may use Raylib, SDL, Qt, GTK, a browser, or another toolkit
that can display through X11.
## Why the pieces are kept separate
Think about a toy car with replaceable batteries. The car should not care which
brand of battery powers it, and the battery should not need to know where the
car is driving.
Agentbox follows the same idea:
- The **Environment interface** is a remote control for a computer:
`Start`, `Launch`, `Screenshot`, `SendInput`, `Logs`, and `Stop`.
- The **Agent interface** receives an observation and chooses the next action.
- The **Runtime** connects the two interfaces.
This lets us swap parts independently:
- Docker today could become a microVM or cloud machine later.
- X11 today could become a Wayland or GPU backend later.
- The fixed test agent could become OpenAI, Claude, Gemini, or a local model.
- The mover game could become a browser, drawing program, or other application.
Only `internal/environment/dockerx11` knows the current Linux tricks. Model
code never imports that package.
## The two current agents
### Deterministic agent
This is a fixed test script:
1. Look at the first screenshot.
2. Hold the RIGHT key.
3. Wait one second.
4. Check another screenshot.
5. Fail unless the green square moved at least 100 pixels.
6. Release the key and finish.
It is intentionally specific to the demo. Its purpose is to test the whole
system without paying for or depending on a model API.
### OpenAI adapter
This optional adapter sends the model:
- the user's task;
- the current screenshot;
- earlier actions;
- recent application logs;
- a short history of the run.
The model must return a small JSON object containing either one allowed action
or `done`. It cannot directly call Docker or run shell commands. The adapter is
one example, not a giant framework for every model company.
## How an application gets inside
`agentbox run` accepts a prebuilt Linux executable.
If the given path is a directory, an `agentbox.json` file says which executable
to run, what arguments to pass, which environment variables to set, and
optionally which window title to wait for.
Agentbox streams the executable into temporary memory inside the container. It
does not share the developer's whole folder with the container. The temporary
copy disappears when the run ends.
There is one practical limit: the executable and its libraries must work in the
Debian-based runtime image. Agentbox does not yet package missing libraries or
convert Windows and macOS programs.
## What gets recorded
The trace is an experiment notebook:
- `run.json` says what ran, which agent controlled it, and whether it finished;
- `steps.jsonl` records every observation, decision, and action in order;
- `actions.jsonl` is a smaller action-only list;
- `screenshots/` contains the pictures the agent saw;
- `stdout.log` and `stderr.log` contain application messages.
The trace format has a version number. Screenshots are separate files instead
of giant blobs inside JSON. This keeps traces easy to inspect and leaves room
for replay, comparisons, tests, or training data later.
## Is the container safe?
Not safe enough for strangers' programs.
Docker is more like a locked bedroom than a bank vault. It keeps ordinary
programs apart, but every container still shares the host's Linux kernel. A
serious kernel or Docker bug might let a hostile program break out.
This prototype adds useful guardrails:
- no network access inside the environment;
- a read-only main filesystem;
- a small, temporary writable area;
- an unprivileged user;
- no extra Linux capabilities;
- limits on CPU, memory, and process count;
- automatic destruction after the run.
These rules reduce accidents. They do not make Docker a trustworthy boundary
for arbitrary customer code. Do not use this prototype to run unknown binaries
on an important machine or a machine shared by multiple customers.
## What production would need
Before accepting untrusted uploads, the room needs walls built with hardware
virtualization. A small virtual machine, such as Firecracker or Kata
Containers, gives each run its own kernel. gVisor may be another option when it
supports the applications we need.
A real service would also need to:
- build uploaded programs away from runtime hosts;
- limit upload size, expanded file size, disk use, runtime, and network access;
- keep every customer's files, logs, secrets, and encryption keys separate;
- authenticate every control request;
- patch and replace base images regularly;
- monitor hosts and destroy every machine after its run.
## What this experiment proves
It proves the interaction model:
1. launch a normal graphical Linux application without a physical monitor;
2. observe it through screenshots and logs;
3. control it with keyboard and mouse actions;
4. keep the environment and agent replaceable;
5. save enough information to understand what happened;
6. shut everything down cleanly.
It does **not** prove production security, GPU game support, cloud scaling,
Windows/macOS support, billing, or authentication.
## Small glossary
- **Container:** A lightweight box around processes and files.
- **Backend:** One implementation hidden behind a common interface.
- **X11:** A Linux system for drawing windows and handling input.
- **Headless:** Running without a physical monitor.
- **Synthetic input:** Keyboard or mouse events created by software.
- **Trace:** The saved record of a run.
- **Kernel:** The deepest part of the operating system that controls hardware
and processes.
- **MicroVM:** A small virtual machine with its own kernel.
## Sources
- [X.Org Xvfb manual](https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml)
- [xdotool project and XTEST behavior](https://github.com/jordansissel/xdotool)
- [Docker Engine security](https://docs.docker.com/engine/security/)
- [gVisor security model](https://gvisor.dev/docs/architecture_guide/security/)
- [Firecracker design](https://firecracker-microvm.github.io/)
+31
View File
@@ -0,0 +1,31 @@
FROM debian:bookworm-slim AS builder
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
libx11-dev \
&& rm -rf /var/lib/apt/lists/*
COPY examples/mover/main.c /src/main.c
RUN cc -std=c11 -O2 -Wall -Wextra -Werror /src/main.c -lX11 -o /mover
FROM debian:bookworm-slim
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
libx11-6 \
openbox \
scrot \
x11-utils \
xdotool \
xvfb \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --uid 10001 agentbox
COPY --from=builder /mover /opt/agentbox/mover
COPY environment/entrypoint.sh /opt/agentbox/entrypoint.sh
RUN chmod 0755 /opt/agentbox/entrypoint.sh /opt/agentbox/mover
USER agentbox
ENV DISPLAY=:99
ENTRYPOINT ["/opt/agentbox/entrypoint.sh"]
+63
View File
@@ -0,0 +1,63 @@
#!/bin/sh
set -eu
export HOME=/tmp/home
export XDG_RUNTIME_DIR=/tmp/runtime
mkdir -p "$HOME" "$XDG_RUNTIME_DIR"
chmod 0700 "$XDG_RUNTIME_DIR"
cleanup() {
rm -f /tmp/agentbox-ready
kill "${OPENBOX_PID:-}" "${XVFB_PID:-}" 2>/dev/null || true
wait "${OPENBOX_PID:-}" "${XVFB_PID:-}" 2>/dev/null || true
}
terminate() {
trap - EXIT INT TERM
cleanup
exit 0
}
trap cleanup EXIT
trap terminate INT TERM
Xvfb "$DISPLAY" -screen 0 640x480x24 -nolisten tcp -noreset &
XVFB_PID=$!
attempt=0
until xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; do
attempt=$((attempt + 1))
if [ "$attempt" -ge 100 ]; then
echo "Xvfb did not become ready" >&2
exit 1
fi
sleep 0.05
done
openbox --sm-disable >/tmp/openbox.log 2>&1 &
OPENBOX_PID=$!
attempt=0
while :; do
wm_info=$(xprop -root _NET_SUPPORTING_WM_CHECK 2>/dev/null || true)
case "$wm_info" in
*"window id #"*) break ;;
esac
if ! kill -0 "$OPENBOX_PID" 2>/dev/null; then
echo "Openbox exited before becoming ready" >&2
exit 1
fi
attempt=$((attempt + 1))
if [ "$attempt" -ge 100 ]; then
echo "Openbox did not become ready" >&2
exit 1
fi
sleep 0.05
done
touch /tmp/agentbox-ready
while :; do
sleep 3600 &
wait $!
done
+4
View File
@@ -0,0 +1,4 @@
{
"command": "mover",
"window_title": "Agentbox Mover"
}
+136
View File
@@ -0,0 +1,136 @@
#define _POSIX_C_SOURCE 199309L
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
enum {
WINDOW_WIDTH = 640,
WINDOW_HEIGHT = 480,
SQUARE_SIZE = 50,
};
static double monotonic_seconds(void) {
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
perror("clock_gettime");
exit(1);
}
return (double)now.tv_sec + (double)now.tv_nsec / 1000000000.0;
}
static void set_key_state(KeySym key, bool down, bool *left, bool *right,
bool *up, bool *down_key) {
switch (key) {
case XK_Left:
*left = down;
break;
case XK_Right:
*right = down;
break;
case XK_Up:
*up = down;
break;
case XK_Down:
*down_key = down;
break;
default:
break;
}
}
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0);
Display *display = XOpenDisplay(NULL);
if (display == NULL) {
fputs("unable to open X display\n", stderr);
return 1;
}
int screen = DefaultScreen(display);
Window window = XCreateSimpleWindow(
display, RootWindow(display, screen), 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
0, BlackPixel(display, screen), BlackPixel(display, screen));
XStoreName(display, window, "Agentbox Mover");
XSelectInput(display, window,
ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask);
XMapWindow(display, window);
GC gc = XCreateGC(display, window, 0, NULL);
Colormap colors = DefaultColormap(display, screen);
XColor green;
XColor red;
if (!XParseColor(display, colors, "#00ff66", &green) ||
!XAllocColor(display, colors, &green) ||
!XParseColor(display, colors, "#ff3355", &red) ||
!XAllocColor(display, colors, &red)) {
fputs("unable to allocate colors\n", stderr);
XCloseDisplay(display);
return 1;
}
double x = (WINDOW_WIDTH - SQUARE_SIZE) / 2.0;
double y = (WINDOW_HEIGHT - SQUARE_SIZE) / 2.0;
bool left = false;
bool right = false;
bool up = false;
bool down = false;
bool marker_visible = false;
int marker_x = 0;
int marker_y = 0;
double previous = monotonic_seconds();
printf("mover started at x=%.1f y=%.1f\n", x, y);
for (;;) {
while (XPending(display) > 0) {
XEvent event;
XNextEvent(display, &event);
if (event.type == KeyPress || event.type == KeyRelease) {
set_key_state(XLookupKeysym(&event.xkey, 0),
event.type == KeyPress, &left, &right, &up, &down);
} else if (event.type == ButtonPress) {
marker_visible = true;
marker_x = event.xbutton.x;
marker_y = event.xbutton.y;
printf("mouse click at x=%d y=%d\n", marker_x, marker_y);
}
}
double now = monotonic_seconds();
double delta = now - previous;
previous = now;
const double speed = 180.0;
x += ((right ? 1.0 : 0.0) - (left ? 1.0 : 0.0)) * speed * delta;
y += ((down ? 1.0 : 0.0) - (up ? 1.0 : 0.0)) * speed * delta;
if (x < 0.0) {
x = 0.0;
} else if (x > WINDOW_WIDTH - SQUARE_SIZE) {
x = WINDOW_WIDTH - SQUARE_SIZE;
}
if (y < 0.0) {
y = 0.0;
} else if (y > WINDOW_HEIGHT - SQUARE_SIZE) {
y = WINDOW_HEIGHT - SQUARE_SIZE;
}
XClearWindow(display, window);
XSetForeground(display, gc, green.pixel);
XFillRectangle(display, window, gc, (int)x, (int)y, SQUARE_SIZE,
SQUARE_SIZE);
if (marker_visible) {
XSetForeground(display, gc, red.pixel);
XFillRectangle(display, window, gc, marker_x - 4, marker_y - 4, 9,
9);
}
XFlush(display);
struct timespec frame = {.tv_sec = 0, .tv_nsec = 16000000};
nanosleep(&frame, NULL);
}
}
+3
View File
@@ -0,0 +1,3 @@
module agentbox
go 1.22
+42
View File
@@ -0,0 +1,42 @@
package agent
import (
"context"
"time"
"agentbox/internal/environment"
)
type Observation struct {
Screenshot []byte
Timestamp time.Time
Logs []environment.LogEntry
PreviousActions []environment.InputAction
}
// Step is the compact history passed back to an agent on its next decision.
type Step struct {
Number int
Timestamp time.Time
ScreenshotPath string
Message string
Action *environment.InputAction
}
// Decision contains either one action or Done. Returning neither is invalid.
type Decision struct {
Reason string
Action *environment.InputAction
Done bool
}
// Agent chooses one backend-neutral action from the latest observation.
type Agent interface {
Name() string
NextAction(
ctx context.Context,
task string,
history []Step,
observation Observation,
) (Decision, error)
}
+93
View File
@@ -0,0 +1,93 @@
package agent
import (
"bytes"
"context"
"fmt"
"image/png"
"agentbox/internal/environment"
)
type Deterministic struct {
nextStep int
initialSquareX float64
hasInitialPosition bool
}
func (deterministicAgent *Deterministic) Name() string {
return "deterministic-right"
}
func (deterministicAgent *Deterministic) NextAction(
_ context.Context,
_ string,
_ []Step,
observation Observation,
) (Decision, error) {
var decision Decision
switch deterministicAgent.nextStep {
case 0:
initialSquareX, err := greenCentroidX(observation.Screenshot)
if err != nil {
return Decision{}, fmt.Errorf("inspect initial screenshot: %w", err)
}
deterministicAgent.initialSquareX = initialSquareX
deterministicAgent.hasInitialPosition = true
action := environment.InputAction{Type: environment.KeyDown, Key: "RIGHT"}
decision = Decision{Reason: "Press RIGHT to start moving.", Action: &action}
case 1:
action := environment.InputAction{Type: environment.Wait, DurationMS: 1000}
decision = Decision{Reason: "Keep RIGHT held for one second.", Action: &action}
case 2:
currentSquareX, err := greenCentroidX(observation.Screenshot)
if err != nil {
return Decision{}, fmt.Errorf("inspect moved screenshot: %w", err)
}
distanceMoved := currentSquareX - deterministicAgent.initialSquareX
if !deterministicAgent.hasInitialPosition || distanceMoved < 100 {
return Decision{}, fmt.Errorf(
"visual verification failed: square moved %.1f pixels right, want at least 100",
distanceMoved,
)
}
action := environment.InputAction{Type: environment.KeyUp, Key: "RIGHT"}
decision = Decision{
Reason: fmt.Sprintf(
"The square moved %.1f pixels right; release RIGHT.",
distanceMoved,
),
Action: &action,
}
default:
decision = Decision{Reason: "The movement sequence is complete.", Done: true}
}
deterministicAgent.nextStep++
return decision, nil
}
func greenCentroidX(screenshot []byte) (float64, error) {
renderedImage, err := png.Decode(bytes.NewReader(screenshot))
if err != nil {
return 0, err
}
var xCoordinateSum, greenPixelCount uint64
bounds := renderedImage.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
red, green, blue, _ := renderedImage.At(x, y).RGBA()
// Match the mover's #00ff66 square. Requiring 1,000 matching pixels
// prevents a small green UI detail from passing verification.
if green > 0xc000 && red < 0x4000 && blue < 0x8000 {
xCoordinateSum += uint64(x)
greenPixelCount++
}
}
}
if greenPixelCount < 1000 {
return 0, fmt.Errorf("found only %d green square pixels", greenPixelCount)
}
return float64(xCoordinateSum) / float64(greenPixelCount), nil
}
var _ Agent = (*Deterministic)(nil)
@@ -0,0 +1,75 @@
package agent
import (
"bytes"
"context"
"image"
"image/color"
"image/png"
"testing"
"agentbox/internal/environment"
)
func TestDeterministicSequence(t *testing.T) {
controller := &Deterministic{}
want := []struct {
action environment.InputType
done bool
}{
{environment.KeyDown, false},
{environment.Wait, false},
{environment.KeyUp, false},
{"", true},
}
for index, expected := range want {
x := 20
if index >= 2 {
x = 140
}
decision, err := controller.NextAction(context.Background(), "", nil, Observation{
Screenshot: screenshotWithSquare(t, x),
})
if err != nil {
t.Fatalf("step %d: %v", index, err)
}
if decision.Done != expected.done {
t.Fatalf("step %d done = %v, want %v", index, decision.Done, expected.done)
}
if expected.action != "" && (decision.Action == nil || decision.Action.Type != expected.action) {
t.Fatalf("step %d action = %#v, want %s", index, decision.Action, expected.action)
}
}
}
func TestDeterministicRejectsMissingMovement(t *testing.T) {
controller := &Deterministic{}
for step := 0; step < 2; step++ {
if _, err := controller.NextAction(context.Background(), "", nil, Observation{
Screenshot: screenshotWithSquare(t, 20),
}); err != nil {
t.Fatal(err)
}
}
if _, err := controller.NextAction(context.Background(), "", nil, Observation{
Screenshot: screenshotWithSquare(t, 20),
}); err == nil {
t.Fatal("movement verification error = nil")
}
}
func screenshotWithSquare(t *testing.T, startX int) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, 240, 100))
green := color.RGBA{G: 255, B: 102, A: 255}
for y := 20; y < 60; y++ {
for x := startX; x < startX+40; x++ {
img.Set(x, y, green)
}
}
var output bytes.Buffer
if err := png.Encode(&output, img); err != nil {
t.Fatal(err)
}
return output.Bytes()
}
+98
View File
@@ -0,0 +1,98 @@
package agent
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type OpenAIConfig struct {
APIKey string
Model string
BaseURL string
Client *http.Client
}
// OpenAI adapts the Responses API to Agentbox's provider-neutral Agent
// contract. HTTP and wire-format details do not leak into the runtime.
type OpenAI struct {
config OpenAIConfig
}
func NewOpenAI(config OpenAIConfig) (*OpenAI, error) {
if config.APIKey == "" {
return nil, errors.New("OPENAI_API_KEY is required for --agent openai")
}
if config.Model == "" {
config.Model = "gpt-5"
}
if config.BaseURL == "" {
config.BaseURL = "https://api.openai.com/v1"
}
if config.Client == nil {
config.Client = &http.Client{Timeout: 90 * time.Second}
}
return &OpenAI{config: config}, nil
}
func (openAI *OpenAI) Name() string {
return "openai:" + openAI.config.Model
}
func (openAI *OpenAI) NextAction(
ctx context.Context,
task string,
history []Step,
observation Observation,
) (Decision, error) {
requestBody, err := buildResponsesRequest(
openAI.config.Model,
task,
history,
observation,
)
if err != nil {
return Decision{}, err
}
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
strings.TrimRight(openAI.config.BaseURL, "/")+"/responses",
bytes.NewReader(requestBody),
)
if err != nil {
return Decision{}, err
}
request.Header.Set("Authorization", "Bearer "+openAI.config.APIKey)
request.Header.Set("Content-Type", "application/json")
httpResponse, err := openAI.config.Client.Do(request)
if err != nil {
return Decision{}, err
}
defer httpResponse.Body.Close()
responseBody, err := io.ReadAll(io.LimitReader(httpResponse.Body, 4<<20))
if err != nil {
return Decision{}, err
}
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
return Decision{}, fmt.Errorf(
"OpenAI response %s: %s",
httpResponse.Status,
strings.TrimSpace(string(responseBody)),
)
}
outputText, err := extractResponseText(responseBody)
if err != nil {
return Decision{}, err
}
return parseModelDecision(outputText)
}
var _ Agent = (*OpenAI)(nil)
+206
View File
@@ -0,0 +1,206 @@
package agent
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"agentbox/internal/environment"
)
type responsesRequest struct {
Model string `json:"model"`
Input []responsesInput `json:"input"`
Text responsesTextSettings `json:"text"`
}
type responsesInput struct {
Role string `json:"role"`
Content []responsesContent `json:"content"`
}
type responsesContent struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
ImageURL string `json:"image_url,omitempty"`
}
type responsesTextSettings struct {
Format responsesFormat `json:"format"`
}
type responsesFormat struct {
Type string `json:"type"`
Name string `json:"name"`
Strict bool `json:"strict"`
Schema map[string]any `json:"schema"`
}
func buildResponsesRequest(
model string,
task string,
history []Step,
observation Observation,
) ([]byte, error) {
prompt, err := modelPrompt(task, history, observation)
if err != nil {
return nil, err
}
request := responsesRequest{
Model: model,
Input: []responsesInput{{
Role: "user",
Content: []responsesContent{
{Type: "input_text", Text: prompt},
{
Type: "input_image",
ImageURL: "data:image/png;base64," +
base64.StdEncoding.EncodeToString(observation.Screenshot),
},
},
}},
Text: responsesTextSettings{Format: responsesFormat{
Type: "json_schema",
Name: "agentbox_action",
Strict: true,
Schema: decisionSchema(),
}},
}
return json.Marshal(request)
}
func modelPrompt(task string, history []Step, observation Observation) (string, error) {
contextData := struct {
Task string `json:"task"`
History []Step `json:"history"`
RecentLogs []environment.LogEntry `json:"recent_logs"`
PreviousActions []environment.InputAction `json:"previous_actions"`
}{
Task: task,
History: history,
RecentLogs: observation.Logs,
PreviousActions: observation.PreviousActions,
}
data, err := json.Marshal(contextData)
if err != nil {
return "", err
}
return "You control an interactive Linux application from screenshots. " +
"Choose exactly one allowed input action, or mark done when the task is complete. " +
"Use logical key names such as RIGHT, ENTER, or ESCAPE. Keep waits under 5000 ms.\n" +
string(data), nil
}
func decisionSchema() map[string]any {
// Strict structured output requires every action property to be present,
// even when a particular action ignores most of them.
actionProperties := map[string]any{
"type": map[string]any{
"type": "string",
"enum": []string{
string(environment.KeyDown), string(environment.KeyUp),
string(environment.MouseMove), string(environment.MouseDown),
string(environment.MouseUp), string(environment.Wait),
},
},
"key": map[string]any{"type": "string"},
"x": map[string]any{"type": "integer"},
"y": map[string]any{"type": "integer"},
"button": map[string]any{"type": "integer"},
"duration_ms": map[string]any{"type": "integer"},
}
return map[string]any{
"type": "object",
"additionalProperties": false,
"properties": map[string]any{
"reason": map[string]any{"type": "string"},
"done": map[string]any{"type": "boolean"},
"action": map[string]any{
"anyOf": []any{
map[string]any{
"type": "object",
"additionalProperties": false,
"properties": actionProperties,
"required": []string{
"type", "key", "x", "y", "button", "duration_ms",
},
},
map[string]any{"type": "null"},
},
},
},
"required": []string{"reason", "done", "action"},
}
}
func extractResponseText(data []byte) (string, error) {
var response struct {
Output []struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
} `json:"output"`
}
if err := json.Unmarshal(data, &response); err != nil {
return "", fmt.Errorf("decode OpenAI response: %w", err)
}
for _, output := range response.Output {
for _, content := range output.Content {
if content.Type == "output_text" && content.Text != "" {
return content.Text, nil
}
}
}
return "", errors.New("OpenAI response contained no output_text")
}
type modelDecision struct {
Reason string `json:"reason"`
Done bool `json:"done"`
Action *modelAction `json:"action"`
}
type modelAction struct {
Type environment.InputType `json:"type"`
Key string `json:"key"`
X int `json:"x"`
Y int `json:"y"`
Button int `json:"button"`
DurationMS int `json:"duration_ms"`
}
func parseModelDecision(text string) (Decision, error) {
var modelOutput modelDecision
if err := json.Unmarshal([]byte(text), &modelOutput); err != nil {
return Decision{}, fmt.Errorf("decode model decision: %w", err)
}
if modelOutput.Reason == "" {
return Decision{}, errors.New("model decision requires reason")
}
if modelOutput.Done {
return Decision{Reason: modelOutput.Reason, Done: true}, nil
}
if modelOutput.Action == nil {
return Decision{}, errors.New("model decision requires action when not done")
}
if modelOutput.Action.DurationMS < 0 || modelOutput.Action.DurationMS > 5000 {
return Decision{}, errors.New("model wait must be between 0 and 5000 ms")
}
action := environment.InputAction{
Type: modelOutput.Action.Type,
Key: modelOutput.Action.Key,
X: modelOutput.Action.X,
Y: modelOutput.Action.Y,
Button: modelOutput.Action.Button,
DurationMS: modelOutput.Action.DurationMS,
}
switch action.Type {
case environment.KeyDown, environment.KeyUp, environment.MouseMove,
environment.MouseDown, environment.MouseUp, environment.Wait:
default:
return Decision{}, fmt.Errorf("model returned unsupported action %q", action.Type)
}
return Decision{Reason: modelOutput.Reason, Action: &action}, nil
}
+59
View File
@@ -0,0 +1,59 @@
package agent
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"agentbox/internal/environment"
)
func TestOpenAISendsScreenshotAndParsesDecision(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/responses" {
t.Errorf("path = %q, want /responses", request.URL.Path)
}
if request.Header.Get("Authorization") != "Bearer test-key" {
t.Errorf("missing bearer token")
}
body, err := io.ReadAll(request.Body)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(body), "data:image/png;base64,AQID") {
t.Errorf("request does not contain screenshot data URL: %s", body)
}
response.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(response, `{
"output":[{"content":[{"type":"output_text","text":"{\"reason\":\"move right\",\"done\":false,\"action\":{\"type\":\"key_down\",\"key\":\"RIGHT\",\"x\":0,\"y\":0,\"button\":0,\"duration_ms\":0}}"}]}]
}`)
}))
defer server.Close()
controller, err := NewOpenAI(OpenAIConfig{
APIKey: "test-key", Model: "test-model", BaseURL: server.URL, Client: server.Client(),
})
if err != nil {
t.Fatal(err)
}
decision, err := controller.NextAction(context.Background(), "move", nil, Observation{
Screenshot: []byte{1, 2, 3},
})
if err != nil {
t.Fatal(err)
}
if decision.Action == nil || decision.Action.Type != environment.KeyDown ||
decision.Action.Key != "RIGHT" {
t.Fatalf("decision = %#v", decision)
}
}
func TestDecisionSchemaIsJSONSerializable(t *testing.T) {
if _, err := json.Marshal(decisionSchema()); err != nil {
t.Fatal(err)
}
}
+66
View File
@@ -0,0 +1,66 @@
package appspec
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"agentbox/internal/environment"
)
type manifest struct {
Command string `json:"command"`
Args []string `json:"args"`
EnvironmentVariables map[string]string `json:"env"`
WindowTitle string `json:"window_title"`
}
// Resolve turns either an executable path or a directory containing
// agentbox.json into the backend-neutral launch command.
func Resolve(path string) (environment.Command, error) {
absolute, err := filepath.Abs(path)
if err != nil {
return environment.Command{}, fmt.Errorf("resolve application path: %w", err)
}
info, err := os.Stat(absolute)
if err != nil {
return environment.Command{}, fmt.Errorf("inspect application path: %w", err)
}
if !info.IsDir() {
if !info.Mode().IsRegular() {
return environment.Command{}, errors.New("application must be a regular executable file")
}
return environment.Command{Path: absolute}, nil
}
data, err := os.ReadFile(filepath.Join(absolute, "agentbox.json"))
if err != nil {
return environment.Command{}, fmt.Errorf("read directory manifest agentbox.json: %w", err)
}
var config manifest
if err := json.Unmarshal(data, &config); err != nil {
return environment.Command{}, fmt.Errorf("parse agentbox.json: %w", err)
}
if config.Command == "" {
return environment.Command{}, errors.New("agentbox.json requires command")
}
commandPath := config.Command
if !filepath.IsAbs(commandPath) {
commandPath = filepath.Join(absolute, commandPath)
}
commandInfo, err := os.Stat(commandPath)
if err != nil {
return environment.Command{}, fmt.Errorf("inspect manifest command: %w", err)
}
if !commandInfo.Mode().IsRegular() {
return environment.Command{}, errors.New("manifest command must be a regular executable file")
}
return environment.Command{
Path: commandPath,
Args: config.Args,
EnvironmentVariables: config.EnvironmentVariables,
WindowTitle: config.WindowTitle,
}, nil
}
+28
View File
@@ -0,0 +1,28 @@
package appspec
import (
"os"
"path/filepath"
"testing"
)
func TestResolveDirectoryManifest(t *testing.T) {
directory := t.TempDir()
executable := filepath.Join(directory, "game")
if err := os.WriteFile(executable, []byte("binary"), 0o755); err != nil {
t.Fatal(err)
}
manifest := `{"command":"game","args":["--demo"],"window_title":"Demo"}`
if err := os.WriteFile(filepath.Join(directory, "agentbox.json"), []byte(manifest), 0o644); err != nil {
t.Fatal(err)
}
command, err := Resolve(directory)
if err != nil {
t.Fatal(err)
}
if command.Path != executable || command.WindowTitle != "Demo" ||
len(command.Args) != 1 || command.Args[0] != "--demo" {
t.Fatalf("command = %#v", command)
}
}
@@ -0,0 +1,175 @@
package dockerx11
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
"agentbox/internal/environment"
)
const stagedApplicationPath = "/tmp/application"
func (environmentBackend *Environment) Launch(
ctx context.Context,
application environment.Command,
) error {
fmt.Fprintln(environmentBackend.config.Output, "Staging application...")
executable, err := os.Open(application.Path)
if err != nil {
return fmt.Errorf("open application: %w", err)
}
defer executable.Close()
// Stream instead of bind-mounting the developer's directory. The container
// sees only the requested executable, and the copy disappears at teardown.
stageCommand := exec.CommandContext(
ctx,
"docker",
"exec",
"-i",
environmentBackend.containerName,
"sh",
"-c",
"cat > "+stagedApplicationPath+" && chmod 0500 "+stagedApplicationPath,
)
stageCommand.Stdin = executable
if output, err := stageCommand.CombinedOutput(); err != nil {
return fmt.Errorf(
"stage application: %w: %s",
err,
strings.TrimSpace(string(output)),
)
}
fmt.Fprintln(environmentBackend.config.Output, "Launching application...")
dockerArguments := []string{"exec", "-d"}
for variableName, variableValue := range application.EnvironmentVariables {
dockerArguments = append(
dockerArguments,
"-e",
variableName+"="+variableValue,
)
}
dockerArguments = append(
dockerArguments,
environmentBackend.containerName,
"sh",
"-c",
)
shellCommand := []string{"exec", stagedApplicationPath}
for _, applicationArgument := range application.Args {
shellCommand = append(shellCommand, shellQuote(applicationArgument))
}
shellCommand = append(
shellCommand,
">/tmp/stdout.log",
"2>/tmp/stderr.log",
)
dockerArguments = append(dockerArguments, strings.Join(shellCommand, " "))
if _, err := environmentBackend.runDocker(ctx, dockerArguments...); err != nil {
return fmt.Errorf("launch application: %w", err)
}
return environmentBackend.focusApplicationWindow(ctx, application.WindowTitle)
}
func (environmentBackend *Environment) focusApplicationWindow(
ctx context.Context,
windowTitle string,
) error {
if windowTitle == "" {
// Applications without a title cannot be searched reliably. Give the
// process a brief startup window before the first screenshot.
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(500 * time.Millisecond):
return nil
}
}
var windowID string
if err := waitFor(ctx, 10*time.Second, func() bool {
output, searchErr := environmentBackend.runDocker(
ctx,
"exec",
environmentBackend.containerName,
"xdotool",
"search",
"--name",
windowTitle,
)
if searchErr != nil {
return false
}
windowID = strings.TrimSpace(strings.Split(string(output), "\n")[0])
return windowID != ""
}); err != nil {
return fmt.Errorf("wait for application window %q: %w", windowTitle, err)
}
if _, err := environmentBackend.runDocker(
ctx,
"exec",
environmentBackend.containerName,
"xdotool",
"windowactivate",
"--sync",
windowID,
); err != nil {
return fmt.Errorf("focus application window: %w", err)
}
return nil
}
func (environmentBackend *Environment) Screenshot(ctx context.Context) ([]byte, error) {
const screenshotPath = "/tmp/screenshot.png"
if _, err := environmentBackend.runDocker(
ctx,
"exec",
environmentBackend.containerName,
"scrot",
"-o",
screenshotPath,
); err != nil {
return nil, fmt.Errorf("capture screenshot: %w", err)
}
screenshot, err := environmentBackend.runDocker(
ctx,
"exec",
environmentBackend.containerName,
"cat",
screenshotPath,
)
if err != nil {
return nil, fmt.Errorf("extract screenshot: %w", err)
}
return screenshot, nil
}
func (environmentBackend *Environment) Logs(
ctx context.Context,
) ([]environment.LogEntry, error) {
var logEntries []environment.LogEntry
for _, streamName := range []string{"stdout", "stderr"} {
logContents, err := environmentBackend.runDocker(
ctx,
"exec",
environmentBackend.containerName,
"cat",
"/tmp/"+streamName+".log",
)
if err != nil || len(logContents) == 0 {
continue
}
logEntries = append(logEntries, environment.LogEntry{
Stream: streamName,
Message: string(logContents),
Time: time.Now().UTC(),
})
}
return logEntries, nil
}
@@ -0,0 +1,61 @@
package dockerx11
import (
"context"
"errors"
"fmt"
"os/exec"
"strings"
"time"
)
func (environmentBackend *Environment) runDocker(
ctx context.Context,
arguments ...string,
) ([]byte, error) {
command := exec.CommandContext(ctx, "docker", arguments...)
output, err := command.CombinedOutput()
if err != nil {
return output, fmt.Errorf(
"docker %s: %w: %s",
arguments[0],
err,
strings.TrimSpace(string(output)),
)
}
return output, nil
}
func (environmentBackend *Environment) streamDockerOutput(
ctx context.Context,
arguments ...string,
) error {
command := exec.CommandContext(ctx, "docker", arguments...)
command.Stdout = environmentBackend.config.Output
command.Stderr = environmentBackend.config.Output
return command.Run()
}
func waitFor(ctx context.Context, timeout time.Duration, ready func() bool) error {
timeoutTimer := time.NewTimer(timeout)
defer timeoutTimer.Stop()
retryTicker := time.NewTicker(100 * time.Millisecond)
defer retryTicker.Stop()
for {
if ready() {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timeoutTimer.C:
return errors.New("timed out")
case <-retryTicker.C:
}
}
}
func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}
@@ -0,0 +1,37 @@
package dockerx11
import (
"io"
"sync"
"agentbox/internal/environment"
)
const runtimeImageName = "agentbox-runtime:local"
// Config contains host-side values needed to create one isolated desktop.
type Config struct {
ProjectRoot string
RunID string
Output io.Writer
}
// Environment implements the backend-neutral environment contract with one
// Docker container, one X11 display, and one application process.
type Environment struct {
config Config
containerName string
stopMutex sync.Mutex
containerCreated bool
containerStopped bool
}
func New(config Config) *Environment {
return &Environment{
config: config,
containerName: "agentbox-run-" + config.RunID,
}
}
var _ environment.Environment = (*Environment)(nil)
@@ -0,0 +1,18 @@
package dockerx11
import "testing"
func TestX11KeyTranslatesLogicalNames(t *testing.T) {
tests := map[string]string{
"RIGHT": "Right",
"LEFT": "Left",
"ENTER": "Return",
"ESCAPE": "Escape",
"A": "a",
}
for input, want := range tests {
if got := toX11KeyName(input); got != want {
t.Errorf("toX11KeyName(%q) = %q, want %q", input, got, want)
}
}
}
@@ -0,0 +1,95 @@
package dockerx11
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"agentbox/internal/environment"
)
func (environmentBackend *Environment) SendInput(
ctx context.Context,
action environment.InputAction,
) error {
var xdotoolArguments []string
switch action.Type {
case environment.KeyDown:
if action.Key == "" {
return errors.New("key_down requires key")
}
xdotoolArguments = []string{"keydown", toX11KeyName(action.Key)}
case environment.KeyUp:
if action.Key == "" {
return errors.New("key_up requires key")
}
xdotoolArguments = []string{"keyup", toX11KeyName(action.Key)}
case environment.MouseMove:
xdotoolArguments = []string{
"mousemove",
strconv.Itoa(action.X),
strconv.Itoa(action.Y),
}
case environment.MouseDown:
xdotoolArguments = []string{"mousedown", strconv.Itoa(action.Button)}
case environment.MouseUp:
xdotoolArguments = []string{"mouseup", strconv.Itoa(action.Button)}
case environment.Wait:
if action.DurationMS < 0 {
return errors.New("wait duration cannot be negative")
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Duration(action.DurationMS) * time.Millisecond):
return nil
}
default:
return fmt.Errorf("unsupported input action %q", action.Type)
}
dockerArguments := []string{
"exec",
environmentBackend.containerName,
"xdotool",
}
dockerArguments = append(dockerArguments, xdotoolArguments...)
if _, err := environmentBackend.runDocker(ctx, dockerArguments...); err != nil {
return fmt.Errorf("send %s: %w", action.Type, err)
}
return nil
}
// toX11KeyName keeps X11 spellings out of the public action API. Agents can use
// logical names such as RIGHT even though xdotool expects Right.
func toX11KeyName(logicalName string) string {
switch strings.ToUpper(logicalName) {
case "LEFT":
return "Left"
case "RIGHT":
return "Right"
case "UP":
return "Up"
case "DOWN":
return "Down"
case "ENTER", "RETURN":
return "Return"
case "ESC", "ESCAPE":
return "Escape"
case "SPACE":
return "space"
case "TAB":
return "Tab"
case "BACKSPACE":
return "BackSpace"
case "DELETE":
return "Delete"
}
if len(logicalName) == 1 {
return strings.ToLower(logicalName)
}
return logicalName
}
@@ -0,0 +1,121 @@
package dockerx11
import (
"context"
"fmt"
"io"
"path/filepath"
"time"
)
func (environmentBackend *Environment) Start(ctx context.Context) error {
if environmentBackend.config.Output == nil {
environmentBackend.config.Output = io.Discard
}
if _, err := environmentBackend.runDocker(
ctx,
"version",
"--format",
"{{.Server.Version}}",
); err != nil {
return fmt.Errorf("Docker is required and the daemon must be accessible: %w", err)
}
fmt.Fprintln(environmentBackend.config.Output, "Creating environment...")
dockerfilePath := filepath.Join(
environmentBackend.config.ProjectRoot,
"environment",
"Dockerfile",
)
if err := environmentBackend.streamDockerOutput(
ctx,
"build",
"-q",
"-t",
runtimeImageName,
"-f",
dockerfilePath,
environmentBackend.config.ProjectRoot,
); err != nil {
return fmt.Errorf("build environment image: %w", err)
}
// These restrictions reduce accidental damage. They are defense in depth,
// not a safe boundary for hostile customer code; see docs/architecture.md.
_, err := environmentBackend.runDocker(
ctx,
"create",
"--name", environmentBackend.containerName,
"--init",
"--network=none",
"--read-only",
"--tmpfs=/tmp:rw,exec,nosuid,nodev,size=128m",
"--cap-drop=ALL",
"--security-opt=no-new-privileges",
"--pids-limit=128",
"--memory=512m",
"--cpus=1",
runtimeImageName,
)
if err != nil {
return fmt.Errorf("create environment: %w", err)
}
environmentBackend.containerCreated = true
if _, err := environmentBackend.runDocker(
ctx,
"start",
environmentBackend.containerName,
); err != nil {
return fmt.Errorf("start environment: %w", err)
}
// entrypoint.sh creates this file only after both Xvfb and Openbox accept
// requests. It is the readiness handshake between host and container.
if err := waitFor(ctx, 10*time.Second, func() bool {
_, readyErr := environmentBackend.runDocker(
ctx,
"exec",
environmentBackend.containerName,
"test",
"-f",
"/tmp/agentbox-ready",
)
return readyErr == nil
}); err != nil {
return fmt.Errorf("wait for graphical environment: %w", err)
}
return nil
}
// Stop is idempotent because both normal completion and deferred cleanup may
// try to tear down the same environment.
func (environmentBackend *Environment) Stop(ctx context.Context) error {
environmentBackend.stopMutex.Lock()
defer environmentBackend.stopMutex.Unlock()
if !environmentBackend.containerCreated || environmentBackend.containerStopped {
return nil
}
environmentBackend.containerStopped = true
fmt.Fprintln(environmentBackend.config.Output, "Shutting environment down...")
_, stopErr := environmentBackend.runDocker(
ctx,
"stop",
"--time=3",
environmentBackend.containerName,
)
_, removeErr := environmentBackend.runDocker(
ctx,
"rm",
"-f",
environmentBackend.containerName,
)
if stopErr != nil {
return fmt.Errorf("stop environment: %w", stopErr)
}
if removeErr != nil {
return fmt.Errorf("remove environment: %w", removeErr)
}
return nil
}
@@ -0,0 +1,56 @@
package environment
import (
"context"
"time"
)
// Command describes a host executable and how it should start in an
// environment. Environment implementations decide how to stage the file.
type Command struct {
Path string
Args []string
EnvironmentVariables map[string]string
WindowTitle string
}
// InputType identifies one backend-neutral keyboard, mouse, or timing action.
type InputType string
const (
KeyDown InputType = "key_down"
KeyUp InputType = "key_up"
MouseMove InputType = "mouse_move"
MouseDown InputType = "mouse_down"
MouseUp InputType = "mouse_up"
Wait InputType = "wait"
)
// InputAction contains only fields relevant to Type. DurationMS is used by Wait.
type InputAction struct {
Type InputType `json:"type"`
Key string `json:"key,omitempty"`
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
Button int `json:"button,omitempty"`
DurationMS int `json:"duration_ms,omitempty"`
}
// LogEntry is a cumulative snapshot of one application output stream. Time is
// when Agentbox captured the snapshot, not when the application emitted it.
type LogEntry struct {
Stream string `json:"stream"`
Message string `json:"message"`
Time time.Time `json:"time"`
}
// Environment is the programmable-computer boundary used by the runtime.
// Implementations may use containers, virtual machines, or remote hosts.
type Environment interface {
Start(context.Context) error
Launch(context.Context, Command) error
Screenshot(context.Context) ([]byte, error)
SendInput(context.Context, InputAction) error
Logs(context.Context) ([]LogEntry, error)
Stop(context.Context) error
}
+414
View File
@@ -0,0 +1,414 @@
package phase1
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"image/png"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
const imageName = "agentbox-phase1:local"
type point struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
type runRecord struct {
ID string `json:"id"`
Phase string `json:"phase"`
Status string `json:"status"`
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at"`
ContainerEngine string `json:"container_engine"`
Before *point `json:"square_before,omitempty"`
After *point `json:"square_after,omitempty"`
MovementPixels float64 `json:"movement_pixels,omitempty"`
Error string `json:"error,omitempty"`
}
type actionRecord struct {
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"`
Key string `json:"key,omitempty"`
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
Button int `json:"button,omitempty"`
Duration string `json:"duration,omitempty"`
}
type runner struct {
root string
runDir string
containerName string
containerMade bool
actions *os.File
record runRecord
}
func Run(ctx context.Context) (runErr error) {
root, err := findRoot()
if err != nil {
return err
}
id, err := runID()
if err != nil {
return fmt.Errorf("create run ID: %w", err)
}
runDir := filepath.Join(root, ".agentbox", "runs", id)
if err := os.MkdirAll(filepath.Join(runDir, "screenshots"), 0o755); err != nil {
return fmt.Errorf("create run directory: %w", err)
}
actions, err := os.Create(filepath.Join(runDir, "actions.jsonl"))
if err != nil {
return fmt.Errorf("create action trace: %w", err)
}
r := &runner{
root: root,
runDir: runDir,
containerName: "agentbox-phase1-" + id,
actions: actions,
record: runRecord{
ID: id,
Phase: "environment-prototype",
Status: "running",
StartedAt: time.Now().UTC(),
},
}
defer func() {
cleanupErr := r.cleanup()
r.record.FinishedAt = time.Now().UTC()
if runErr == nil && cleanupErr != nil {
runErr = cleanupErr
}
if runErr != nil {
r.record.Status = "failed"
r.record.Error = runErr.Error()
} else {
r.record.Status = "passed"
}
if err := r.writeRecord(); err != nil && runErr == nil {
runErr = err
}
}()
fmt.Println("Checking container engine...")
version, err := r.docker(ctx, "version", "--format", "{{.Server.Version}}")
if err != nil {
return fmt.Errorf("Docker is required and the daemon must be accessible: %w", err)
}
r.record.ContainerEngine = "Docker " + strings.TrimSpace(string(version))
fmt.Println("Building Phase 1 environment...")
if err := r.dockerStream(ctx, "build", "-t", imageName, "-f",
filepath.Join(root, "environment", "Dockerfile"), root); err != nil {
return fmt.Errorf("build environment image: %w", err)
}
fmt.Println("Creating restricted graphical environment...")
_, err = r.docker(ctx,
"create",
"--name", r.containerName,
"--init",
"--network=none",
"--read-only",
"--tmpfs=/tmp:rw,nosuid,nodev,size=64m",
"--cap-drop=ALL",
"--security-opt=no-new-privileges",
"--pids-limit=128",
"--memory=512m",
"--cpus=1",
imageName,
)
if err != nil {
return fmt.Errorf("create environment: %w", err)
}
r.containerMade = true
if _, err := r.docker(ctx, "start", r.containerName); err != nil {
return fmt.Errorf("start environment: %w", err)
}
if err := r.waitFor(ctx, 10*time.Second, func() bool {
_, readyErr := r.docker(ctx, "exec", r.containerName, "test", "-f", "/tmp/agentbox-ready")
return readyErr == nil
}); err != nil {
return fmt.Errorf("wait for graphical environment: %w", err)
}
fmt.Println("Launching graphical mover demo...")
if _, err := r.docker(ctx, "exec", "-d", r.containerName, "sh", "-c",
"exec /opt/agentbox/mover >/tmp/stdout.log 2>/tmp/stderr.log"); err != nil {
return fmt.Errorf("launch demo: %w", err)
}
var windowID string
if err := r.waitFor(ctx, 10*time.Second, func() bool {
output, searchErr := r.docker(ctx, "exec", r.containerName, "xdotool",
"search", "--name", "Agentbox Mover")
if searchErr != nil {
return false
}
windowID = strings.TrimSpace(strings.Split(string(output), "\n")[0])
return windowID != ""
}); err != nil {
return fmt.Errorf("wait for demo window: %w", err)
}
if _, err := r.docker(ctx, "exec", r.containerName, "xdotool",
"windowactivate", "--sync", windowID); err != nil {
return fmt.Errorf("focus demo window: %w", err)
}
beforePath := filepath.Join(r.runDir, "screenshots", "before.png")
fmt.Println("Capturing screenshot before input...")
if err := r.screenshot(ctx, "/tmp/before.png", beforePath); err != nil {
return err
}
before, err := squareCentroid(beforePath)
if err != nil {
return fmt.Errorf("inspect before screenshot: %w", err)
}
r.record.Before = &before
fmt.Println("Sending synthetic mouse input...")
if _, err := r.docker(ctx, "exec", r.containerName, "xdotool",
"mousemove", "--window", windowID, "100", "100", "mousedown", "1", "mouseup", "1"); err != nil {
return fmt.Errorf("send mouse input: %w", err)
}
if err := r.trace(actionRecord{
Timestamp: time.Now().UTC(), Type: "mouse_move", X: 100, Y: 100,
}); err != nil {
return err
}
if err := r.trace(actionRecord{
Timestamp: time.Now().UTC(), Type: "mouse_down", Button: 1,
}); err != nil {
return err
}
if err := r.trace(actionRecord{
Timestamp: time.Now().UTC(), Type: "mouse_up", Button: 1,
}); err != nil {
return err
}
fmt.Println("Holding RIGHT for one second...")
if _, err := r.docker(ctx, "exec", r.containerName, "xdotool", "keydown", "Right"); err != nil {
return fmt.Errorf("send key down: %w", err)
}
if err := r.trace(actionRecord{
Timestamp: time.Now().UTC(), Type: "key_down", Key: "RIGHT",
}); err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
}
if _, err := r.docker(ctx, "exec", r.containerName, "xdotool", "keyup", "Right"); err != nil {
return fmt.Errorf("send key up: %w", err)
}
if err := r.trace(actionRecord{
Timestamp: time.Now().UTC(), Type: "key_up", Key: "RIGHT", Duration: "1s",
}); err != nil {
return err
}
time.Sleep(100 * time.Millisecond)
afterPath := filepath.Join(r.runDir, "screenshots", "after.png")
fmt.Println("Capturing screenshot after input...")
if err := r.screenshot(ctx, "/tmp/after.png", afterPath); err != nil {
return err
}
after, err := squareCentroid(afterPath)
if err != nil {
return fmt.Errorf("inspect after screenshot: %w", err)
}
r.record.After = &after
r.record.MovementPixels = after.X - before.X
if r.record.MovementPixels < 100 {
return fmt.Errorf("visual verification failed: square moved %.1f pixels right, want at least 100", r.record.MovementPixels)
}
fmt.Printf("Verified: square moved %.1f pixels to the right.\n", r.record.MovementPixels)
fmt.Printf("Phase 1 passed. Artifacts: %s\n", runDir)
return nil
}
func findRoot() (string, error) {
current, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("get working directory: %w", err)
}
for {
data, readErr := os.ReadFile(filepath.Join(current, "go.mod"))
if readErr == nil && bytes.Contains(data, []byte("module agentbox")) {
return current, nil
}
parent := filepath.Dir(current)
if parent == current {
return "", errors.New("run from the agentbox module directory")
}
current = parent
}
}
func runID() (string, error) {
random := make([]byte, 3)
if _, err := rand.Read(random); err != nil {
return "", err
}
return time.Now().UTC().Format("20060102T150405") + "-" + hex.EncodeToString(random), nil
}
func (r *runner) waitFor(ctx context.Context, timeout time.Duration, check func() bool) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
if check() {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return errors.New("timed out")
case <-ticker.C:
}
}
}
func (r *runner) screenshot(ctx context.Context, containerPath, hostPath string) error {
if _, err := r.docker(ctx, "exec", r.containerName, "scrot", "-o", containerPath); err != nil {
return fmt.Errorf("capture screenshot: %w", err)
}
if err := r.extract(ctx, containerPath, hostPath); err != nil {
return fmt.Errorf("extract screenshot: %w", err)
}
return nil
}
func (r *runner) extract(ctx context.Context, containerPath, hostPath string) error {
data, err := r.docker(ctx, "exec", r.containerName, "cat", containerPath)
if err != nil {
return err
}
if err := os.WriteFile(hostPath, data, 0o644); err != nil {
return fmt.Errorf("write %s: %w", hostPath, err)
}
return nil
}
func squareCentroid(path string) (point, error) {
file, err := os.Open(path)
if err != nil {
return point{}, err
}
defer file.Close()
img, err := png.Decode(file)
if err != nil {
return point{}, err
}
var sumX, sumY, count uint64
bounds := img.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
red, green, blue, _ := img.At(x, y).RGBA()
// Match the demo square's #00ff66 color with enough tolerance for
// image conversion while rejecting the black background.
if green > 0xc000 && red < 0x4000 && blue < 0x8000 {
sumX += uint64(x)
sumY += uint64(y)
count++
}
}
}
if count < 1000 {
return point{}, fmt.Errorf("found only %d green square pixels", count)
}
return point{
X: float64(sumX) / float64(count),
Y: float64(sumY) / float64(count),
}, nil
}
func (r *runner) trace(action actionRecord) error {
encoded, err := json.Marshal(action)
if err != nil {
return fmt.Errorf("encode action: %w", err)
}
if _, err := r.actions.Write(append(encoded, '\n')); err != nil {
return fmt.Errorf("write action trace: %w", err)
}
return nil
}
func (r *runner) cleanup() error {
if r.actions != nil {
_ = r.actions.Close()
}
if !r.containerMade {
return nil
}
fmt.Println("Stopping environment...")
stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
for _, name := range []string{"stdout.log", "stderr.log"} {
_ = r.extract(stopCtx, "/tmp/"+name, filepath.Join(r.runDir, name))
}
_, stopErr := r.docker(stopCtx, "stop", "--time=3", r.containerName)
_, removeErr := r.docker(stopCtx, "rm", "-f", r.containerName)
if stopErr != nil {
return fmt.Errorf("stop environment: %w", stopErr)
}
if removeErr != nil {
return fmt.Errorf("remove environment: %w", removeErr)
}
fmt.Println("Environment stopped cleanly.")
return nil
}
func (r *runner) writeRecord() error {
data, err := json.MarshalIndent(r.record, "", " ")
if err != nil {
return fmt.Errorf("encode run record: %w", err)
}
data = append(data, '\n')
if err := os.WriteFile(filepath.Join(r.runDir, "run.json"), data, 0o644); err != nil {
return fmt.Errorf("write run record: %w", err)
}
return nil
}
func (r *runner) docker(ctx context.Context, args ...string) ([]byte, error) {
command := exec.CommandContext(ctx, "docker", args...)
output, err := command.CombinedOutput()
if err != nil {
return output, fmt.Errorf("docker %s: %w: %s", args[0], err, strings.TrimSpace(string(output)))
}
return output, nil
}
func (r *runner) dockerStream(ctx context.Context, args ...string) error {
command := exec.CommandContext(ctx, "docker", args...)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
if err := command.Run(); err != nil {
return fmt.Errorf("docker %s: %w", strconv.Quote(strings.Join(args, " ")), err)
}
return nil
}
+54
View File
@@ -0,0 +1,54 @@
package phase1
import (
"image"
"image/color"
"image/png"
"os"
"path/filepath"
"testing"
)
func TestSquareCentroid(t *testing.T) {
path := filepath.Join(t.TempDir(), "screenshot.png")
img := image.NewRGBA(image.Rect(0, 0, 200, 100))
green := color.RGBA{R: 0, G: 255, B: 102, A: 255}
for y := 20; y < 60; y++ {
for x := 80; x < 120; x++ {
img.Set(x, y, green)
}
}
writePNG(t, path, img)
got, err := squareCentroid(path)
if err != nil {
t.Fatalf("squareCentroid() error = %v", err)
}
if got.X != 99.5 || got.Y != 39.5 {
t.Fatalf("squareCentroid() = (%.1f, %.1f), want (99.5, 39.5)", got.X, got.Y)
}
}
func TestSquareCentroidRejectsMissingSquare(t *testing.T) {
path := filepath.Join(t.TempDir(), "empty.png")
writePNG(t, path, image.NewRGBA(image.Rect(0, 0, 200, 100)))
if _, err := squareCentroid(path); err == nil {
t.Fatal("squareCentroid() error = nil, want missing-square error")
}
}
func writePNG(t *testing.T, path string, img image.Image) {
t.Helper()
file, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
if err := png.Encode(file, img); err != nil {
_ = file.Close()
t.Fatal(err)
}
if err := file.Close(); err != nil {
t.Fatal(err)
}
}
+182
View File
@@ -0,0 +1,182 @@
package runtime
import (
"context"
"errors"
"fmt"
"io"
"time"
"agentbox/internal/agent"
"agentbox/internal/environment"
"agentbox/internal/trace"
)
// Config wires replaceable environment, agent, and trace implementations into
// one run. The runtime itself has no Docker, X11, or model-provider knowledge.
type Config struct {
Task string
Command environment.Command
Controller agent.Agent
Environment environment.Environment
TraceStore *trace.Store
MaxSteps int
Output io.Writer
}
func Run(ctx context.Context, config Config) (runErr error) {
if config.MaxSteps <= 0 {
config.MaxSteps = 20
}
if config.Output == nil {
config.Output = io.Discard
}
if config.Controller == nil || config.Environment == nil || config.TraceStore == nil {
return errors.New("runtime requires agent, environment, and trace")
}
defer func() {
runErr = finalizeRun(config, runErr)
}()
if err := config.Environment.Start(ctx); err != nil {
return err
}
if err := config.Environment.Launch(ctx, config.Command); err != nil {
return err
}
fmt.Fprintln(config.Output, "Agent loop started.")
var stepHistory []agent.Step
var actionHistory []environment.InputAction
runStartedAt := time.Now()
for stepNumber := 1; stepNumber <= config.MaxSteps; stepNumber++ {
screenshot, err := config.Environment.Screenshot(ctx)
if err != nil {
return err
}
screenshotPath, err := config.TraceStore.SaveScreenshot(stepNumber, screenshot)
if err != nil {
return err
}
applicationLogs, err := config.Environment.Logs(ctx)
if err != nil {
return err
}
observedAt := time.Now().UTC()
previousActions := append([]environment.InputAction(nil), actionHistory...)
observation := agent.Observation{
Screenshot: screenshot,
Timestamp: observedAt,
Logs: applicationLogs,
PreviousActions: previousActions,
}
fmt.Fprintf(config.Output, "[%s] screenshot captured\n", elapsed(runStartedAt))
decision, err := config.Controller.NextAction(
ctx,
config.Task,
stepHistory,
observation,
)
if err != nil {
return fmt.Errorf("agent next action: %w", err)
}
fmt.Fprintf(
config.Output,
"[%s] agent: %s\n",
elapsed(runStartedAt),
decision.Reason,
)
if err := config.TraceStore.Record(trace.StepRecord{
Step: stepNumber,
Timestamp: observedAt,
Observation: trace.ObservationRecord{
Screenshot: screenshotPath,
Logs: applicationLogs,
PreviousActions: previousActions,
},
Agent: trace.AgentRecord{Message: decision.Reason},
Action: decision.Action,
Done: decision.Done,
}); err != nil {
return err
}
stepHistory = append(stepHistory, agent.Step{
Number: stepNumber,
Timestamp: observedAt,
ScreenshotPath: screenshotPath,
Message: decision.Reason,
Action: decision.Action,
})
if decision.Done {
fmt.Fprintln(config.Output, "Task complete.")
return nil
}
if decision.Action == nil {
return errors.New("agent returned neither action nor completion")
}
fmt.Fprintf(
config.Output,
"[%s] action: %s%s\n",
elapsed(runStartedAt),
decision.Action.Type,
actionDetail(*decision.Action),
)
if err := config.Environment.SendInput(ctx, *decision.Action); err != nil {
return err
}
actionHistory = append(actionHistory, *decision.Action)
}
return fmt.Errorf("agent exceeded maximum of %d steps", config.MaxSteps)
}
// finalizeRun uses a fresh timeout because the caller's context may already be
// canceled. Preserving logs and removing the environment must still be tried.
func finalizeRun(
config Config,
runErr error,
) error {
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if logs, err := config.Environment.Logs(cleanupCtx); err == nil {
if logErr := config.TraceStore.WriteLogs(logs); runErr == nil && logErr != nil {
runErr = logErr
}
}
// Stop is idempotent, so always call it. Start may have created a container
// before returning an error while waiting for its graphical services.
if stopErr := config.Environment.Stop(cleanupCtx); runErr == nil && stopErr != nil {
runErr = stopErr
}
if finishErr := config.TraceStore.Finish(runErr); runErr == nil && finishErr != nil {
runErr = finishErr
}
return runErr
}
func elapsed(start time.Time) string {
duration := time.Since(start).Round(time.Second)
return fmt.Sprintf(
"%02d:%02d",
int(duration.Minutes()),
int(duration.Seconds())%60,
)
}
func actionDetail(action environment.InputAction) string {
switch action.Type {
case environment.KeyDown, environment.KeyUp:
return " " + action.Key
case environment.MouseMove:
return fmt.Sprintf(" %d,%d", action.X, action.Y)
case environment.MouseDown, environment.MouseUp:
return fmt.Sprintf(" %d", action.Button)
case environment.Wait:
return fmt.Sprintf(" %dms", action.DurationMS)
default:
return ""
}
}
+63
View File
@@ -0,0 +1,63 @@
package runtime
import (
"context"
"errors"
"io"
"testing"
"agentbox/internal/agent"
"agentbox/internal/environment"
"agentbox/internal/trace"
)
type failingStartEnvironment struct {
stopCalled bool
}
func (fake *failingStartEnvironment) Start(context.Context) error {
return errors.New("startup failed")
}
func (fake *failingStartEnvironment) Launch(context.Context, environment.Command) error {
return nil
}
func (fake *failingStartEnvironment) Screenshot(context.Context) ([]byte, error) {
return nil, nil
}
func (fake *failingStartEnvironment) SendInput(context.Context, environment.InputAction) error {
return nil
}
func (fake *failingStartEnvironment) Logs(context.Context) ([]environment.LogEntry, error) {
return nil, nil
}
func (fake *failingStartEnvironment) Stop(context.Context) error {
fake.stopCalled = true
return nil
}
func TestRunStopsEnvironmentWhenStartFails(t *testing.T) {
traceStore, err := trace.New(t.TempDir(), "task", "application", "test-agent")
if err != nil {
t.Fatal(err)
}
fakeEnvironment := &failingStartEnvironment{}
err = Run(context.Background(), Config{
Task: "task",
Controller: &agent.Deterministic{},
Environment: fakeEnvironment,
TraceStore: traceStore,
Output: io.Discard,
})
if err == nil {
t.Fatal("Run() error = nil, want startup error")
}
if !fakeEnvironment.stopCalled {
t.Fatal("Run() did not stop environment after startup failure")
}
}
+4
View File
@@ -0,0 +1,4 @@
// Package trace stores durable run records. steps.jsonl contains complete
// observations and decisions, actions.jsonl is an action-only view, and PNG
// screenshots remain separate files referenced by relative paths.
package trace
+97
View File
@@ -0,0 +1,97 @@
package trace
import (
"bufio"
"encoding/json"
"errors"
"os"
"path/filepath"
"sort"
)
func List(projectRoot string) ([]Run, error) {
runsDirectory := filepath.Join(projectRoot, ".agentbox", "runs")
directoryEntries, err := os.ReadDir(runsDirectory)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, err
}
var runs []Run
for _, entry := range directoryEntries {
if !entry.IsDir() {
continue
}
run, err := Read(projectRoot, entry.Name())
// Old or malformed trace directories are intentionally hidden rather
// than making the entire run listing fail.
if err == nil && run.SchemaVersion == SchemaVersion {
runs = append(runs, run)
}
}
sort.Slice(runs, func(left, right int) bool {
return runs[left].StartedAt.After(runs[right].StartedAt)
})
return runs, nil
}
func Read(projectRoot, runID string) (Run, error) {
if err := validateRunID(runID); err != nil {
return Run{}, err
}
runPath := filepath.Join(
projectRoot,
".agentbox",
"runs",
runID,
"run.json",
)
data, err := os.ReadFile(runPath)
if err != nil {
return Run{}, err
}
var run Run
if err := json.Unmarshal(data, &run); err != nil {
return Run{}, err
}
return run, nil
}
func ReadSteps(projectRoot, runID string) ([]StepRecord, error) {
if err := validateRunID(runID); err != nil {
return nil, err
}
stepsPath := filepath.Join(
projectRoot,
".agentbox",
"runs",
runID,
"steps.jsonl",
)
stepsFile, err := os.Open(stepsPath)
if err != nil {
return nil, err
}
defer stepsFile.Close()
var steps []StepRecord
scanner := bufio.NewScanner(stepsFile)
for scanner.Scan() {
var step StepRecord
if err := json.Unmarshal(scanner.Bytes(), &step); err != nil {
return nil, err
}
steps = append(steps, step)
}
return steps, scanner.Err()
}
func validateRunID(runID string) error {
// Run IDs become path components, so reject separators and traversal.
if filepath.Base(runID) != runID {
return errors.New("invalid run ID")
}
return nil
}
+180
View File
@@ -0,0 +1,180 @@
package trace
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"agentbox/internal/environment"
)
// Store writes one run's metadata and append-only event streams.
type Store struct {
runDirectory string
run Run
stepsFile *os.File
actionsFile *os.File
}
func New(projectRoot, task, application, agentName string) (*Store, error) {
runID, err := newRunID()
if err != nil {
return nil, err
}
runDirectory := filepath.Join(projectRoot, ".agentbox", "runs", runID)
screenshotDirectory := filepath.Join(runDirectory, "screenshots")
if err := os.MkdirAll(screenshotDirectory, 0o755); err != nil {
return nil, fmt.Errorf("create run directory: %w", err)
}
stepsFile, err := os.Create(filepath.Join(runDirectory, "steps.jsonl"))
if err != nil {
return nil, fmt.Errorf("create step trace: %w", err)
}
actionsFile, err := os.Create(filepath.Join(runDirectory, "actions.jsonl"))
if err != nil {
_ = stepsFile.Close()
return nil, fmt.Errorf("create action trace: %w", err)
}
store := &Store{
runDirectory: runDirectory,
stepsFile: stepsFile,
actionsFile: actionsFile,
run: Run{
SchemaVersion: SchemaVersion,
ID: runID,
Task: task,
Application: application,
Agent: agentName,
Status: "running",
StartedAt: time.Now().UTC(),
},
}
if err := store.writeRunSummary(); err != nil {
_ = stepsFile.Close()
_ = actionsFile.Close()
return nil, err
}
return store, nil
}
func (store *Store) ID() string {
return store.run.ID
}
func (store *Store) Directory() string {
return store.runDirectory
}
func (store *Store) SaveScreenshot(stepNumber int, screenshot []byte) (string, error) {
relativePath := filepath.Join(
"screenshots",
fmt.Sprintf("%04d.png", stepNumber),
)
absolutePath := filepath.Join(store.runDirectory, relativePath)
if err := os.WriteFile(absolutePath, screenshot, 0o644); err != nil {
return "", fmt.Errorf("write screenshot: %w", err)
}
// Trace paths always use slash separators so traces are portable.
return filepath.ToSlash(relativePath), nil
}
func (store *Store) Record(step StepRecord) error {
if err := appendJSONLine(store.stepsFile, step); err != nil {
return fmt.Errorf("record step: %w", err)
}
if step.Action != nil {
action := actionRecord{
Step: step.Step,
Timestamp: step.Timestamp,
Action: *step.Action,
}
if err := appendJSONLine(store.actionsFile, action); err != nil {
return fmt.Errorf("record action: %w", err)
}
}
store.run.StepCount = step.Step
return store.writeRunSummary()
}
func (store *Store) WriteLogs(logEntries []environment.LogEntry) error {
var standardOutput, standardError string
for _, entry := range logEntries {
switch entry.Stream {
case "stdout":
standardOutput = entry.Message
case "stderr":
standardError = entry.Message
}
}
if err := os.WriteFile(
filepath.Join(store.runDirectory, "stdout.log"),
[]byte(standardOutput),
0o644,
); err != nil {
return err
}
return os.WriteFile(
filepath.Join(store.runDirectory, "stderr.log"),
[]byte(standardError),
0o644,
)
}
func (store *Store) Finish(runErr error) error {
store.closeEventFiles()
store.run.FinishedAt = time.Now().UTC()
if runErr != nil {
store.run.Status = "failed"
store.run.Error = runErr.Error()
} else {
store.run.Status = "complete"
}
return store.writeRunSummary()
}
func (store *Store) closeEventFiles() {
if store.stepsFile != nil {
_ = store.stepsFile.Close()
store.stepsFile = nil
}
if store.actionsFile != nil {
_ = store.actionsFile.Close()
store.actionsFile = nil
}
}
// Sync each JSONL record so an interrupted run retains its latest complete
// decision. Performance is secondary to debuggability in this local spike.
func appendJSONLine(file *os.File, value any) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
if _, err := file.Write(append(data, '\n')); err != nil {
return err
}
return file.Sync()
}
func (store *Store) writeRunSummary() error {
data, err := json.MarshalIndent(store.run, "", " ")
if err != nil {
return err
}
runPath := filepath.Join(store.runDirectory, "run.json")
return os.WriteFile(runPath, append(data, '\n'), 0o644)
}
func newRunID() (string, error) {
randomSuffix := make([]byte, 3)
if _, err := rand.Read(randomSuffix); err != nil {
return "", err
}
timestamp := time.Now().UTC().Format("20060102T150405")
return timestamp + "-" + hex.EncodeToString(randomSuffix), nil
}
+50
View File
@@ -0,0 +1,50 @@
package trace
import (
"time"
"agentbox/internal/environment"
)
// SchemaVersion changes only when existing trace readers need new logic.
const SchemaVersion = "1"
// Run is the summary stored in run.json.
type Run struct {
SchemaVersion string `json:"schema_version"`
ID string `json:"id"`
Task string `json:"task"`
Application string `json:"application"`
Agent string `json:"agent"`
Status string `json:"status"`
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at,omitempty"`
StepCount int `json:"step_count"`
Error string `json:"error,omitempty"`
}
type ObservationRecord struct {
Screenshot string `json:"screenshot"`
Logs []environment.LogEntry `json:"logs,omitempty"`
PreviousActions []environment.InputAction `json:"previous_actions,omitempty"`
}
type AgentRecord struct {
Message string `json:"message"`
}
// StepRecord is one append-only line in steps.jsonl.
type StepRecord struct {
Step int `json:"step"`
Timestamp time.Time `json:"timestamp"`
Observation ObservationRecord `json:"observation"`
Agent AgentRecord `json:"agent"`
Action *environment.InputAction `json:"action,omitempty"`
Done bool `json:"done,omitempty"`
}
type actionRecord struct {
Step int `json:"step"`
Timestamp time.Time `json:"timestamp"`
Action environment.InputAction `json:"action"`
}
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
image=agentbox-runtime:local
output="$root/examples/mover/mover"
temporary="$output.tmp"
cleanup() {
rm -f "$temporary"
}
trap cleanup EXIT INT TERM
docker build -q -t "$image" -f "$root/environment/Dockerfile" "$root" >/dev/null
docker run --rm --network=none --entrypoint cat "$image" /opt/agentbox/mover >"$temporary"
chmod 0755 "$temporary"
mv "$temporary" "$output"
-102
View File
@@ -1,102 +0,0 @@
package main
import "core:fmt"
import "core:os"
import eng "pkg:engine"
import sdl "vendor:sdl3"
PERF_ITERATIONS :: #config(PERF_ITERATIONS, 2_000_000)
PERF_WARMUP :: #config(PERF_WARMUP, 10_000)
PERF_TRIALS :: #config(PERF_TRIALS, 7)
@(private)
median :: proc(values: []f64) -> f64 {
for i in 1 ..< len(values) {
value := values[i]
j := i
for j > 0 {
if values[j - 1] <= value do break
values[j] = values[j - 1]
j -= 1
}
values[j] = value
}
middle := len(values) / 2
if len(values) & 1 == 1 do return values[middle]
return (values[middle - 1] + values[middle]) / 2
}
@(private)
run_draws :: proc(app: ^eng.App, sprite: ^eng.Sprite, iterations: int) -> u64 {
clear(&app.draw_list)
start := sdl.GetTicksNS()
for i in 0 ..< iterations {
if len(app.draw_list) >= eng.MAX_SPRITES {
clear(&app.draw_list)
}
// Vary an input so optimized builds cannot hoist the draw calculations.
sprite.position.x = f32(i & 1023)
eng.draw_sprite(app, sprite)
}
return sdl.GetTicksNS() - start
}
main :: proc() {
#assert(PERF_ITERATIONS > 0)
#assert(PERF_WARMUP > 0)
#assert(PERF_TRIALS > 0)
file_data, err := os.read_entire_file(
"assets_baked/characters/toad/toad.char.json",
context.allocator,
)
if err != nil {
fmt.eprintfln("benchmark asset read failed: %v", err)
return
}
defer delete(file_data)
def, ok := eng.parse_char_def(file_data)
if !ok do return
data := eng.Character_Data {
def = def,
texture = cast(^sdl.GPUTexture)uintptr(1),
width = 1911,
height = 1526,
}
app := eng.App {
cmd = cast(^sdl.GPUCommandBuffer)uintptr(1),
swapchain_texture = cast(^sdl.GPUTexture)uintptr(2),
swapchain_w = 800,
swapchain_h = 600,
camera = eng.camera_default(),
draw_list = make(
[dynamic]eng.Queued_Sprite,
0,
eng.MAX_SPRITES,
),
}
defer delete(app.draw_list)
sprite := eng.spawn_sprite(&data, {400, 400}, "walk", 4)
_ = run_draws(&app, &sprite, PERF_WARMUP)
samples: [PERF_TRIALS]f64
fmt.printfln(
"benchmark=draw_sprite iterations=%d warmup=%d trials=%d sprites_per_queue=%d",
PERF_ITERATIONS,
PERF_WARMUP,
PERF_TRIALS,
eng.MAX_SPRITES,
)
for trial in 0 ..< PERF_TRIALS {
elapsed := run_draws(&app, &sprite, PERF_ITERATIONS)
samples[trial] = f64(elapsed) / f64(PERF_ITERATIONS)
fmt.printfln("trial=%d ns_per_draw=%.3f", trial + 1, samples[trial])
}
fmt.printfln("median_ns_per_draw=%.3f", median(samples[:]))
}
-174
View File
@@ -1,174 +0,0 @@
package main
import "core:fmt"
import eng "pkg:engine"
import sdl "vendor:sdl3"
PERF_SPRITES :: #config(PERF_SPRITES, 128)
PERF_FRAMES :: #config(PERF_FRAMES, 400)
PERF_WARMUP_FRAMES :: #config(PERF_WARMUP_FRAMES, 100)
PERF_TRIALS :: #config(PERF_TRIALS, 10)
PERF_SCENARIO :: #config(PERF_SCENARIO, 0)
PERF_WIDTH :: #config(PERF_WIDTH, 800)
PERF_HEIGHT :: #config(PERF_HEIGHT, 600)
Scenario :: enum {
Visible,
Half_Offscreen,
Alternating_Textures,
Stacked,
}
@(private)
median :: proc(values: []f64) -> f64 {
for i in 1 ..< len(values) {
value := values[i]
j := i
for j > 0 {
if values[j - 1] <= value do break
values[j] = values[j - 1]
j -= 1
}
values[j] = value
}
middle := len(values) / 2
if len(values) & 1 == 1 do return values[middle]
return (values[middle - 1] + values[middle]) / 2
}
@(private)
scenario_name :: proc(scenario: Scenario) -> string {
switch scenario {
case .Visible:
return "visible"
case .Half_Offscreen:
return "half_offscreen"
case .Alternating_Textures:
return "alternating_textures"
case .Stacked:
return "stacked"
}
return "unknown"
}
@(private)
present_mode_name :: proc(app: ^eng.App) -> string {
if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .IMMEDIATE) {
return "immediate"
}
if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .MAILBOX) {
return "mailbox"
}
return "vsync"
}
@(private)
draw_frame :: proc(app: ^eng.App, sprites: []eng.Sprite) {
for &sprite in sprites {
eng.update_sprite(&sprite, 1.0 / 60.0)
}
eng.begin_frame(app)
for &sprite in sprites {
eng.draw_sprite(app, &sprite)
}
eng.end_frame(app)
}
main :: proc() {
#assert(PERF_SPRITES > 0)
#assert(PERF_SPRITES <= eng.MAX_SPRITES)
#assert(PERF_FRAMES > 0)
#assert(PERF_WARMUP_FRAMES > 0)
#assert(PERF_TRIALS > 0)
#assert(PERF_SCENARIO >= 0 && PERF_SCENARIO <= 3)
#assert(PERF_WIDTH > 0)
#assert(PERF_HEIGHT > 0)
scenario := Scenario(PERF_SCENARIO)
app: eng.App
if !eng.init(&app, "sprite frame benchmark", PERF_WIDTH, PERF_HEIGHT) {
return
}
defer eng.shutdown(&app)
path := "assets_baked/characters/toad/toad.char.json"
data_a, ok := eng.load_character_data(&app, path)
if !ok do return
defer eng.destroy_character_data(&app, &data_a)
data_b: eng.Character_Data
data_b, ok = eng.load_character_data(&app, path)
if !ok do return
defer eng.destroy_character_data(&app, &data_b)
sprites: [PERF_SPRITES]eng.Sprite
for i in 0 ..< PERF_SPRITES {
data := &data_a
if scenario == .Alternating_Textures && (i & 1) == 1 {
data = &data_b
}
position := eng.Vec2 {
f32(40 + (i % 16) * 48),
f32(120 + (i / 16) * 60),
}
if scenario == .Half_Offscreen && (i & 1) == 1 {
position = {-10_000, -10_000}
} else if scenario == .Stacked {
position = {f32(PERF_WIDTH / 2), f32(PERF_HEIGHT / 2)}
}
sprites[i] = eng.spawn_sprite(data, position, "walk", i % 17)
}
for _ in 0 ..< PERF_WARMUP_FRAMES {
draw_frame(&app, sprites[:])
}
if !sdl.WaitForGPUIdle(app.device) {
fmt.eprintfln("GPU wait failed after warm-up: %s", sdl.GetError())
return
}
fmt.printfln(
"benchmark=sprite_frame scenario=%s sprites=%d resolution=%dx%d frames=%d warmup=%d trials=%d backend=%v driver=%s present=%s",
scenario_name(scenario),
PERF_SPRITES,
PERF_WIDTH,
PERF_HEIGHT,
PERF_FRAMES,
PERF_WARMUP_FRAMES,
PERF_TRIALS,
app.shader.backend,
sdl.GetGPUDeviceDriver(app.device),
present_mode_name(&app),
)
samples: [PERF_TRIALS]f64
for trial in 0 ..< PERF_TRIALS {
start := sdl.GetTicksNS()
for _ in 0 ..< PERF_FRAMES {
draw_frame(&app, sprites[:])
}
if !sdl.WaitForGPUIdle(app.device) {
fmt.eprintfln("GPU wait failed after trial %d: %s", trial + 1, sdl.GetError())
return
}
elapsed := sdl.GetTicksNS() - start
samples[trial] = f64(elapsed) / f64(PERF_FRAMES) / 1_000_000.0
fmt.printfln(
"trial=%d ms_per_frame=%.3f fps=%.1f",
trial + 1,
samples[trial],
1_000.0 / samples[trial],
)
}
median_ms := median(samples[:])
fmt.printfln(
"median_ms_per_frame=%.3f median_fps=%.1f",
median_ms,
1_000.0 / median_ms,
)
}
Executable
BIN
View File
Binary file not shown.
+57
View File
@@ -118,6 +118,63 @@ set_sprite_clip_switch_and_guards :: proc(t: ^testing.T) {
testing.expect_value(t, sprite.time, f32(0.09))
}
@(test)
set_sprite_clip_caches_clip_def :: proc(t: ^testing.T) {
data := make_test_character()
defer destroy_test_character(&data)
sprite := spawn_sprite(&data, {}, "idle", 0)
testing.expect(t, sprite.has_clip, "spawn should cache a valid clip")
testing.expect_value(t, sprite.clip, "idle")
testing.expect(t, sprite.clip_def.loop, "idle clip loops")
testing.expect_value(t, sprite.clip_def.fps, f32(10))
testing.expect_value(t, len(sprite.clip_def.frames), 3)
set_sprite_clip(&sprite, "once")
testing.expect(t, sprite.has_clip, "switch should refresh cache")
testing.expect_value(t, sprite.clip, "once")
testing.expect(t, !sprite.clip_def.loop, "once clip does not loop")
testing.expect_value(t, sprite.clip_def.fps, f32(10))
testing.expect_value(t, len(sprite.clip_def.frames), 3)
set_sprite_clip(&sprite, "nope")
testing.expect(t, sprite.has_clip, "bad clip must leave cache intact")
testing.expect_value(t, sprite.clip, "once")
testing.expect(t, !sprite.clip_def.loop, "cached once clip preserved")
testing.expect_value(t, len(sprite.clip_def.frames), 3)
}
@(test)
set_sprite_clip_def_applies_and_guards :: proc(t: ^testing.T) {
data := make_test_character()
defer destroy_test_character(&data)
sprite := spawn_sprite(&data, {}, "idle", 0)
sprite.frame = 2
sprite.time = 0.05
once := data.def.clips["once"]
set_sprite_clip_def(&sprite, "once", once)
testing.expect_value(t, sprite.clip, "once")
testing.expect(t, sprite.has_clip, "def apply should mark clip present")
testing.expect_value(t, sprite.frame, 0)
testing.expect_value(t, sprite.time, f32(0))
testing.expect(t, !sprite.clip_def.loop, "once clip does not loop")
testing.expect_value(t, len(sprite.clip_def.frames), 3)
sprite.frame = 1
sprite.time = 0.09
set_sprite_clip_def(&sprite, "once", once)
testing.expect_value(t, sprite.frame, 1)
testing.expect_value(t, sprite.time, f32(0.09))
set_sprite_clip_def(&sprite, "empty", Clip_Def{loop = true, fps = 10, frames = nil})
testing.expect_value(t, sprite.clip, "once")
testing.expect_value(t, sprite.frame, 1)
set_sprite_clip_def(nil, "once", once)
}
@(test)
spawn_sprite_valid_and_invalid :: proc(t: ^testing.T) {
data := make_test_character()
+137 -12
View File
@@ -16,8 +16,15 @@ SPRITE_VERTS_SIZE :: SPRITE_VERT_COUNT * size_of(Vertex)
VERTEX_BUFFER_SIZE :: MAX_SPRITES * SPRITE_VERTS_SIZE
Queued_Sprite :: struct {
texture: ^sdl.GPUTexture,
verts: [SPRITE_VERT_COUNT]Vertex,
batch_group: u32, // 0 = strict order; nonzero = caller permits regrouping
}
Draw_Batch :: struct {
start: int,
count: int,
texture: ^sdl.GPUTexture,
verts: [SPRITE_VERT_COUNT]Vertex,
}
App :: struct {
@@ -65,7 +72,7 @@ init :: proc(app: ^App, title: cstring, width, height: i32) -> bool {
}
requested: sdl.GPUShaderFormat = {.SPIRV, .DXIL, .MSL}
app.device = sdl.CreateGPUDevice(requested, true, nil)
app.device = sdl.CreateGPUDevice(requested, false, nil)
if app.device == nil {
fmt.eprintfln("CreateGPUDevice failed: %s", sdl.GetError())
return false
@@ -76,6 +83,10 @@ init :: proc(app: ^App, title: cstring, width, height: i32) -> bool {
return false
}
if !sdl.SetGPUAllowedFramesInFlight(app.device, 3) {
fmt.eprintfln("SetGPUAllowedFramesInFlight failed: %s", sdl.GetError())
}
// Prefer uncapped present for profiling; fall back if unsupported.
present := sdl.GPUPresentMode.VSYNC
if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .IMMEDIATE) {
@@ -242,7 +253,11 @@ end_frame :: proc(app: ^App) {
}
n := len(app.draw_list)
batches: [dynamic]Draw_Batch
defer delete(batches)
if n > 0 {
prepare_draw_batches(app.draw_list[:], &batches)
map_ptr := sdl.MapGPUTransferBuffer(app.device, app.transfer_buffer, false)
if map_ptr == nil {
fmt.eprintfln("MapGPUTransferBuffer failed: %s", sdl.GetError())
@@ -286,26 +301,20 @@ end_frame :: proc(app: ^App) {
app.render_pass = sdl.BeginGPURenderPass(cmd, &color_info, 1, nil)
sdl.BindGPUGraphicsPipeline(app.render_pass, app.pipeline)
i := 0
for i < n {
run := texture_run_len(app.draw_list[:], i)
q0 := app.draw_list[i]
for batch in batches {
sampler_binding := sdl.GPUTextureSamplerBinding {
texture = q0.texture,
texture = batch.texture,
sampler = app.sampler,
}
sdl.BindGPUFragmentSamplers(app.render_pass, 0, &sampler_binding, 1)
vb_binding := sdl.GPUBufferBinding {
buffer = app.vertex_buffer,
offset = u32(i * SPRITE_VERTS_SIZE),
offset = u32(batch.start * SPRITE_VERTS_SIZE),
}
sdl.BindGPUVertexBuffers(app.render_pass, 0, &vb_binding, 1)
sdl.DrawGPUPrimitives(app.render_pass, u32(run * SPRITE_VERT_COUNT), 1, 0, 0)
i += run
sdl.DrawGPUPrimitives(app.render_pass, u32(batch.count * SPRITE_VERT_COUNT), 1, 0, 0)
}
sdl.EndGPURenderPass(app.render_pass)
app.render_pass = nil
@@ -454,3 +463,119 @@ texture_run_len :: proc(list: []Queued_Sprite, start: int) -> int {
return n
}
texture_run_count :: proc(list: []Queued_Sprite) -> int {
if len(list) == 0 do return 0
count := 0
i := 0
for i < len(list) {
run := texture_run_len(list, i)
count += 1
i += run
}
return count
}
prepare_draw_batches :: proc(list: []Queued_Sprite, batches: ^[dynamic]Draw_Batch) {
clear(batches)
group_texture_runs(list)
i := 0
for i < len(list) {
run := texture_run_len(list, i)
append(batches, Draw_Batch{start = i, count = run, texture = list[i].texture})
i += run
}
}
// Within each contiguous nonzero batch_group, stably sort by texture so
// consecutive same-texture sprites become one draw. Group 0 and group
// boundaries are never crossed.
group_texture_runs :: proc(list: []Queued_Sprite) {
start := 0
for start < len(list) {
group := list[start].batch_group
if group == 0 {
start += 1
continue
}
end := start + 1
for end < len(list) && list[end].batch_group == group {
end += 1
}
stable_group_by_texture(list[start:end])
start = end
}
}
// Stably orders window by texture pointer. Fast path for 12 textures
// (common); insertion sort for 3+.
stable_group_by_texture :: proc(window: []Queued_Sprite) {
n := len(window)
if n <= 1 do return
already := true
for i in 1 ..< n {
if uintptr(window[i].texture) < uintptr(window[i - 1].texture) {
already = false
break
}
}
if already do return
first := uintptr(window[0].texture)
second: uintptr
has_second := false
third := false
for i in 1 ..< n {
t := uintptr(window[i].texture)
if t == first do continue
if !has_second {
second = t
has_second = true
continue
}
if t != second {
third = true
break
}
}
if !has_second do return
if third {
for i in 1 ..< n {
item := window[i]
j := i
for j > 0 {
if uintptr(window[j - 1].texture) <= uintptr(item.texture) {
break
}
window[j] = window[j - 1]
j -= 1
}
window[j] = item
}
return
}
lo, hi := first, second
if lo > hi do lo, hi = hi, lo
tmp: [MAX_SPRITES]Queued_Sprite
w := 0
for q in window {
if uintptr(q.texture) == lo {
tmp[w] = q
w += 1
}
}
for q in window {
if uintptr(q.texture) == hi {
tmp[w] = q
w += 1
}
}
copy(window, tmp[:w])
}
+365
View File
@@ -7,6 +7,18 @@ fake_tex :: proc(id: uintptr) -> ^sdl.GPUTexture {
return cast(^sdl.GPUTexture)id
}
queued :: proc(tex_id: uintptr, group: u32, marker: f32) -> Queued_Sprite {
q: Queued_Sprite
q.texture = fake_tex(tex_id)
q.batch_group = group
q.verts[0].pos = {marker, 0}
return q
}
marker_of :: proc(q: Queued_Sprite) -> f32 {
return q.verts[0].pos.x
}
@(test)
texture_run_len_empty_or_oob :: proc(t: ^testing.T) {
testing.expect_value(t, texture_run_len(nil, 0), 0)
@@ -62,3 +74,356 @@ texture_run_len_all_different :: proc(t: ^testing.T) {
testing.expect_value(t, texture_run_len(list, 1), 1)
testing.expect_value(t, texture_run_len(list, 2), 1)
}
@(test)
group_texture_runs_strict_order_unchanged :: proc(t: ^testing.T) {
// Group 0 alternates textures; sorting must not reorder (alpha order).
list := []Queued_Sprite {
queued(2, 0, 1),
queued(1, 0, 2),
queued(2, 0, 3),
queued(1, 0, 4),
}
before_runs := texture_run_count(list)
group_texture_runs(list)
testing.expect_value(t, texture_run_count(list), before_runs)
testing.expect_value(t, marker_of(list[0]), f32(1))
testing.expect_value(t, marker_of(list[1]), f32(2))
testing.expect_value(t, marker_of(list[2]), f32(3))
testing.expect_value(t, marker_of(list[3]), f32(4))
}
@(test)
group_texture_runs_reduces_runs_inside_group :: proc(t: ^testing.T) {
list := []Queued_Sprite {
queued(2, 1, 1),
queued(1, 1, 2),
queued(2, 1, 3),
queued(1, 1, 4),
}
testing.expect_value(t, texture_run_count(list), 4)
group_texture_runs(list)
testing.expect_value(t, texture_run_count(list), 2)
testing.expect(t, list[0].texture == fake_tex(1), "lower texture pointer first")
testing.expect(t, list[1].texture == fake_tex(1), "same texture run")
testing.expect(t, list[2].texture == fake_tex(2), "second texture run")
testing.expect(t, list[3].texture == fake_tex(2), "second texture run cont")
}
@(test)
group_texture_runs_stable_same_texture :: proc(t: ^testing.T) {
list := []Queued_Sprite {
queued(2, 1, 10),
queued(1, 1, 20),
queued(2, 1, 30),
queued(1, 1, 40),
}
group_texture_runs(list)
// Same-texture relative order preserved (stable sort).
testing.expect_value(t, marker_of(list[0]), f32(20))
testing.expect_value(t, marker_of(list[1]), f32(40))
testing.expect_value(t, marker_of(list[2]), f32(10))
testing.expect_value(t, marker_of(list[3]), f32(30))
}
@(test)
group_texture_runs_respects_group_boundaries :: proc(t: ^testing.T) {
list := []Queued_Sprite {
queued(2, 1, 1),
queued(1, 1, 2),
queued(2, 0, 3), // strict barrier
queued(1, 2, 4),
queued(2, 2, 5),
}
group_texture_runs(list)
testing.expect_value(t, marker_of(list[2]), f32(3)) // barrier stays put
testing.expect(t, list[0].texture == fake_tex(1))
testing.expect(t, list[1].texture == fake_tex(2))
testing.expect(t, list[3].texture == fake_tex(1))
testing.expect(t, list[4].texture == fake_tex(2))
testing.expect_value(t, list[0].batch_group, u32(1))
testing.expect_value(t, list[1].batch_group, u32(1))
testing.expect_value(t, list[2].batch_group, u32(0))
testing.expect_value(t, list[3].batch_group, u32(2))
testing.expect_value(t, list[4].batch_group, u32(2))
}
@(test)
group_texture_runs_does_not_merge_across_different_groups :: proc(t: ^testing.T) {
// Adjacent nonzero groups with different ids must not merge runs across.
list := []Queued_Sprite {
queued(1, 1, 1),
queued(2, 1, 2),
queued(1, 2, 3),
queued(2, 2, 4),
}
group_texture_runs(list)
testing.expect_value(t, texture_run_count(list), 4)
testing.expect_value(t, list[0].batch_group, u32(1))
testing.expect_value(t, list[1].batch_group, u32(1))
testing.expect_value(t, list[2].batch_group, u32(2))
testing.expect_value(t, list[3].batch_group, u32(2))
}
@(test)
group_texture_runs_empty :: proc(t: ^testing.T) {
list := []Queued_Sprite{}
testing.expect_value(t, texture_run_count(list), 0)
group_texture_runs(list)
testing.expect_value(t, texture_run_count(list), 0)
}
@(test)
group_texture_runs_already_optimal :: proc(t: ^testing.T) {
list := []Queued_Sprite {
queued(1, 1, 10),
queued(1, 1, 20),
queued(2, 1, 30),
queued(2, 1, 40),
}
testing.expect_value(t, texture_run_count(list), 2)
group_texture_runs(list)
testing.expect_value(t, texture_run_count(list), 2)
testing.expect_value(t, marker_of(list[0]), f32(10))
testing.expect_value(t, marker_of(list[1]), f32(20))
testing.expect_value(t, marker_of(list[2]), f32(30))
testing.expect_value(t, marker_of(list[3]), f32(40))
}
@(test)
group_texture_runs_alternating_many_stable :: proc(t: ^testing.T) {
// Characterization: large alternating group must collapse to two runs
// while preserving same-texture submission order (markers).
N :: 64
list := make([]Queued_Sprite, N)
defer delete(list)
for i in 0 ..< N {
tex: uintptr = 2 if (i % 2) == 0 else 1
list[i] = queued(tex, 1, f32(i + 1))
}
testing.expect_value(t, texture_run_count(list), N)
group_texture_runs(list)
testing.expect_value(t, texture_run_count(list), 2)
testing.expect(t, list[0].texture == fake_tex(1))
testing.expect(t, list[N / 2 - 1].texture == fake_tex(1))
testing.expect(t, list[N / 2].texture == fake_tex(2))
testing.expect(t, list[N - 1].texture == fake_tex(2))
for i in 0 ..< N / 2 {
testing.expect_value(t, marker_of(list[i]), f32(2 * i + 2)) // odd markers: 2,4,...,N
testing.expect_value(t, marker_of(list[N / 2 + i]), f32(2 * i + 1)) // even markers: 1,3,...,N-1
}
}
@(test)
group_texture_runs_three_textures_stable :: proc(t: ^testing.T) {
list := []Queued_Sprite {
queued(3, 1, 1),
queued(1, 1, 2),
queued(2, 1, 3),
queued(3, 1, 4),
queued(1, 1, 5),
queued(2, 1, 6),
}
testing.expect_value(t, texture_run_count(list), 6)
group_texture_runs(list)
testing.expect_value(t, texture_run_count(list), 3)
testing.expect_value(t, marker_of(list[0]), f32(2))
testing.expect_value(t, marker_of(list[1]), f32(5))
testing.expect_value(t, marker_of(list[2]), f32(3))
testing.expect_value(t, marker_of(list[3]), f32(6))
testing.expect_value(t, marker_of(list[4]), f32(1))
testing.expect_value(t, marker_of(list[5]), f32(4))
}
@(test)
group_texture_runs_noncontiguous_same_group_id :: proc(t: ^testing.T) {
// Same nonzero id split by group 0: each window regroups alone.
list := []Queued_Sprite {
queued(2, 1, 1),
queued(1, 1, 2),
queued(2, 0, 3),
queued(2, 1, 4),
queued(1, 1, 5),
}
group_texture_runs(list)
testing.expect_value(t, marker_of(list[2]), f32(3))
testing.expect(t, list[0].texture == fake_tex(1))
testing.expect(t, list[1].texture == fake_tex(2))
testing.expect_value(t, list[2].batch_group, u32(0))
testing.expect(t, list[3].texture == fake_tex(1))
testing.expect(t, list[4].texture == fake_tex(2))
testing.expect_value(t, list[0].batch_group, u32(1))
testing.expect_value(t, list[1].batch_group, u32(1))
testing.expect_value(t, list[3].batch_group, u32(1))
testing.expect_value(t, list[4].batch_group, u32(1))
}
make_test_draw_app :: proc() -> App {
app: App
app.cmd = cast(^sdl.GPUCommandBuffer)uintptr(1)
app.swapchain_texture = fake_tex(99)
app.swapchain_w = 800
app.swapchain_h = 600
app.camera = camera_default()
app.draw_list = make([dynamic]Queued_Sprite)
return app
}
destroy_test_draw_app :: proc(app: ^App) {
if app == nil do return
delete(app.draw_list)
app^ = {}
}
make_test_draw_character :: proc() -> Character_Data {
data: Character_Data
data.texture = fake_tex(42)
data.width = 100
data.height = 100
data.def.pivot = {0.5, 1.0}
data.def.clips = make(map[string]Clip_Def)
frames := make([]Frame_Def, 1)
frames[0] = Frame_Def {
rect = {0, 0, 10, 10},
source_size = {10, 10},
trim_offset = {0, 0},
}
data.def.clips["idle"] = Clip_Def {
loop = true,
fps = 10,
frames = frames,
}
return data
}
destroy_test_draw_character :: proc(data: ^Character_Data) {
if data == nil do return
keys := make([dynamic]string, context.temp_allocator)
for key, clip in data.def.clips {
delete(clip.frames)
append(&keys, key)
}
for key in keys {
delete_key(&data.def.clips, key)
}
delete(data.def.clips)
data^ = {}
}
@(test)
draw_sprite_stamps_batch_group_zero :: proc(t: ^testing.T) {
app := make_test_draw_app()
defer destroy_test_draw_app(&app)
data := make_test_draw_character()
defer destroy_test_draw_character(&data)
sprite := spawn_sprite(&data, {100, 200}, "idle", 0)
draw_sprite(&app, &sprite)
testing.expect_value(t, len(app.draw_list), 1)
testing.expect_value(t, app.draw_list[0].batch_group, u32(0))
testing.expect(t, app.draw_list[0].texture == data.texture)
}
@(test)
draw_sprite_at_max_sprites_does_not_append :: proc(t: ^testing.T) {
app := make_test_draw_app()
defer destroy_test_draw_app(&app)
data := make_test_draw_character()
defer destroy_test_draw_character(&data)
for _ in 0 ..< MAX_SPRITES {
append(&app.draw_list, Queued_Sprite{texture = data.texture})
}
testing.expect_value(t, len(app.draw_list), MAX_SPRITES)
sprite := spawn_sprite(&data, {100, 200}, "idle", 0)
draw_sprite(&app, &sprite)
testing.expect_value(t, len(app.draw_list), MAX_SPRITES)
}
@(test)
draw_sprite_batched_stamps_batch_group :: proc(t: ^testing.T) {
app := make_test_draw_app()
defer destroy_test_draw_app(&app)
data := make_test_draw_character()
defer destroy_test_draw_character(&data)
sprite := spawn_sprite(&data, {100, 200}, "idle", 0)
draw_sprite_batched(&app, &sprite, 7)
testing.expect_value(t, len(app.draw_list), 1)
testing.expect_value(t, app.draw_list[0].batch_group, u32(7))
testing.expect(t, app.draw_list[0].texture == data.texture)
}
@(test)
draw_sprite_batched_guards_leave_list_unchanged :: proc(t: ^testing.T) {
app := make_test_draw_app()
defer destroy_test_draw_app(&app)
data := make_test_draw_character()
defer destroy_test_draw_character(&data)
sprite := spawn_sprite(&data, {100, 200}, "idle", 0)
app.cmd = nil
draw_sprite_batched(&app, &sprite, 1)
testing.expect_value(t, len(app.draw_list), 0)
app.cmd = cast(^sdl.GPUCommandBuffer)uintptr(1)
app.swapchain_texture = nil
draw_sprite_batched(&app, &sprite, 1)
testing.expect_value(t, len(app.draw_list), 0)
app.swapchain_texture = fake_tex(99)
draw_sprite_batched(&app, nil, 1)
testing.expect_value(t, len(app.draw_list), 0)
no_data := sprite
no_data.data = nil
draw_sprite_batched(&app, &no_data, 1)
testing.expect_value(t, len(app.draw_list), 0)
no_tex := sprite
tex_data := data
tex_data.texture = nil
no_tex.data = &tex_data
draw_sprite_batched(&app, &no_tex, 1)
testing.expect_value(t, len(app.draw_list), 0)
}
@(test)
prepare_draw_batches_regroups_then_plans_runs :: proc(t: ^testing.T) {
list := []Queued_Sprite {
queued(2, 1, 1),
queued(1, 1, 2),
queued(2, 1, 3),
queued(1, 1, 4),
queued(3, 0, 5),
queued(1, 0, 6),
}
batches := make([dynamic]Draw_Batch)
defer delete(batches)
prepare_draw_batches(list, &batches)
testing.expect_value(t, len(batches), 4)
testing.expect_value(t, batches[0].start, 0)
testing.expect_value(t, batches[0].count, 2)
testing.expect(t, batches[0].texture == fake_tex(1))
testing.expect_value(t, batches[1].start, 2)
testing.expect_value(t, batches[1].count, 2)
testing.expect(t, batches[1].texture == fake_tex(2))
testing.expect_value(t, batches[2].start, 4)
testing.expect_value(t, batches[2].count, 1)
testing.expect(t, batches[2].texture == fake_tex(3))
testing.expect_value(t, batches[3].start, 5)
testing.expect_value(t, batches[3].count, 1)
testing.expect(t, batches[3].texture == fake_tex(1))
// Group 0 submission order preserved.
testing.expect_value(t, marker_of(list[4]), f32(5))
testing.expect_value(t, marker_of(list[5]), f32(6))
testing.expect_value(t, list[4].batch_group, u32(0))
testing.expect_value(t, list[5].batch_group, u32(0))
}
+9
View File
@@ -27,6 +27,15 @@ choose_shader_runtime_prefers_msl :: proc(t: ^testing.T) {
testing.expect_value(t, rt.format, sdl.GPUShaderFormat{.MSL})
}
@(test)
choose_shader_runtime_prefers_dxil_without_msl :: proc(t: ^testing.T) {
rt, ok := choose_shader_runtime_from_formats({.DXIL, .SPIRV})
testing.expect(t, ok, "should pick a runtime when DXIL is available")
testing.expect_value(t, rt.backend, Shader_Backend.DSD12_DXIL)
testing.expect_value(t, rt.shader_dir, "shaders/d3d12")
testing.expect_value(t, rt.format, sdl.GPUShaderFormat{.DXIL})
}
@(test)
choose_shader_runtime_spirv_only :: proc(t: ^testing.T) {
rt, ok := choose_shader_runtime_from_formats({.SPIRV})
+71 -28
View File
@@ -5,12 +5,14 @@ import sdl "vendor:sdl3"
Vec2 :: [2]f32
Sprite :: struct {
data: ^Character_Data,
position: Vec2,
clip: string,
frame: int,
time: f32,
flip_x: bool,
data: ^Character_Data,
position: Vec2,
clip: string,
clip_def: Clip_Def,
has_clip: bool,
frame: int,
time: f32,
flip_x: bool,
}
spawn_sprite :: proc(
@@ -24,16 +26,14 @@ spawn_sprite :: proc(
position = position,
}
set_sprite_clip(&sprite, clip)
if frame != 0 {
c, ok := character_clip(sprite.data, sprite.clip)
if ok {
if frame < 0 {
sprite.frame = 0
} else if frame >= len(c.frames) {
sprite.frame = len(c.frames) - 1
} else {
sprite.frame = frame
}
if frame != 0 && sprite.has_clip {
c := sprite.clip_def
if frame < 0 {
sprite.frame = 0
} else if frame >= len(c.frames) {
sprite.frame = len(c.frames) - 1
} else {
sprite.frame = frame
}
}
return sprite
@@ -42,9 +42,9 @@ spawn_sprite :: proc(
update_sprite :: proc(sprite: ^Sprite, dt: f32) {
if sprite == nil || sprite.data == nil do return
if dt <= 0 do return
if !sprite.has_clip do return
clip, ok := character_clip(sprite.data, sprite.clip)
if !ok do return
clip := sprite.clip_def
frame_count := len(clip.frames)
if frame_count <= 0 do return
@@ -82,7 +82,31 @@ to_clip :: proc(px, py, sw, sh: f32) -> [2]f32 {
}
}
// Axis-aligned quad: two unique x and y values, so scale once and reuse.
sprite_quad_to_clip :: proc(x0, y0, x1, y1, sw, sh: f32) -> [4]Vec2 {
sx := 2.0 / sw
sy := 2.0 / sh
left := x0 * sx - 1
right := x1 * sx - 1
top := 1 - y0 * sy
bottom := 1 - y1 * sy
return {
{left, top},
{right, top},
{right, bottom},
{left, bottom},
}
}
draw_sprite :: proc(app: ^App, sprite: ^Sprite) {
draw_sprite_batched(app, sprite, 0)
}
// Nonzero batch_group lets end_frame regroup consecutive same-group sprites by
// texture. Group 0 keeps exact submission order for correct alpha overlap.
draw_sprite_batched :: proc(app: ^App, sprite: ^Sprite, batch_group: u32) {
if app.cmd == nil || app.swapchain_texture == nil {
return
}
@@ -92,12 +116,13 @@ draw_sprite :: proc(app: ^App, sprite: ^Sprite) {
if len(app.draw_list) >= MAX_SPRITES {
return
}
frame, ok := character_frame(sprite.data, sprite.clip, sprite.frame)
if !ok {
if !sprite.has_clip do return
if sprite.frame < 0 || sprite.frame >= len(sprite.clip_def.frames) {
return
}
frame := sprite.clip_def.frames[sprite.frame]
src_w := f32(frame.source_size[0])
src_h := f32(frame.source_size[1])
if src_w <= 0 do src_w = f32(frame.rect[2])
@@ -124,10 +149,8 @@ draw_sprite :: proc(app: ^App, sprite: ^Sprite) {
sw := f32(app.swapchain_w)
sh := f32(app.swapchain_h)
p0 := to_clip(x0_px, y0_px, sw, sh)
p1 := to_clip(x1_px, y0_px, sw, sh)
p2 := to_clip(x1_px, y1_px, sw, sh)
p3 := to_clip(x0_px, y1_px, sw, sh)
points := sprite_quad_to_clip(x0_px, y0_px, x1_px, y1_px, sw, sh)
p0, p1, p2, p3 := points[0], points[1], points[2], points[3]
tex_w := f32(sprite.data.width)
tex_h := f32(sprite.data.height)
@@ -142,18 +165,38 @@ draw_sprite :: proc(app: ^App, sprite: ^Sprite) {
{pos = p3, uv = {u0, v1}},
}
append(&app.draw_list, Queued_Sprite{texture = sprite.data.texture, verts = verts})
append(
&app.draw_list,
Queued_Sprite {
texture = sprite.data.texture,
verts = verts,
batch_group = batch_group,
},
)
}
set_sprite_clip :: proc(sprite: ^Sprite, clip: string) {
if sprite == nil || sprite.data == nil do return
if sprite.clip == clip do return
if sprite.clip == clip && sprite.has_clip do return
_, ok := character_clip(sprite.data, clip)
def, ok := character_clip(sprite.data, clip)
if !ok do return
set_sprite_clip_def(sprite, clip, def)
}
// Applies a pre-resolved clip without looking up the character clip map.
// Use when callers already hold Clip_Def (e.g. thrashing between known clips).
set_sprite_clip_def :: proc(sprite: ^Sprite, clip: string, def: Clip_Def) {
if sprite == nil do return
if len(def.frames) == 0 do return
if sprite.clip == clip && sprite.has_clip do return
sprite.clip = clip
sprite.clip_def = def
sprite.has_clip = true
sprite.frame = 0
sprite.time = 0
}
+80
View File
@@ -0,0 +1,80 @@
package main
import "core:fmt"
import eng "pkg:engine"
// Clip thrash: flip every sprite between idle/walk each frame via set_sprite_clip_def.
COUNT :: eng.MAX_SPRITES
COLS :: 16
FRAME_LOG_EVERY :: 60
main :: proc() {
app: eng.App
if !eng.init(&app, "clip_thrash", 800, 600) do return
defer eng.shutdown(&app)
data, ok := eng.load_character_data(&app, "assets_baked/characters/toad/toad.char.json")
if !ok do return
defer eng.destroy_character_data(&app, &data)
idle_def, idle_ok := eng.character_clip(&data, "idle")
walk_def, walk_ok := eng.character_clip(&data, "walk")
if !idle_ok || !walk_ok do return
sprites: [COUNT]eng.Sprite
for i in 0 ..< COUNT {
col := i % COLS
row := i / COLS
pos := eng.Vec2 {
f32(40 + col * 48),
f32(80 + row * 60),
}
sprites[i] = eng.spawn_sprite(&data, pos, "idle", i % 5)
}
app.camera.position = {400, 400}
last := eng.now_seconds()
frame_i := 0
sum_ms: f64
peak_ms: f64
use_walk := false
for eng.events() {
now := eng.now_seconds()
dt := f32(now - last)
last = now
frame_ms := f64(dt) * 1000.0
sum_ms += frame_ms
if frame_ms > peak_ms do peak_ms = frame_ms
frame_i += 1
if frame_i % FRAME_LOG_EVERY == 0 {
avg := sum_ms / f64(FRAME_LOG_EVERY)
fps := 1000.0 / avg if avg > 0 else 0
fmt.printfln(
"clip_thrash frame: avg=%.2f ms (%.1f FPS) peak=%.2f ms over %d frames",
avg,
fps,
peak_ms,
FRAME_LOG_EVERY,
)
sum_ms = 0
peak_ms = 0
}
clip := "walk" if use_walk else "idle"
def := walk_def if use_walk else idle_def
use_walk = !use_walk
for &s in sprites {
eng.set_sprite_clip_def(&s, clip, def)
eng.update_sprite(&s, dt)
}
eng.begin_frame(&app)
for &s in sprites {
eng.draw_sprite(&app, &s)
}
eng.end_frame(&app)
}
}
+29 -6
View File
@@ -1,9 +1,12 @@
package main
import "core:fmt"
import eng "pkg:engine"
// Many sprites sharing one Character_Data (Flyweight) good batching demo.
COUNT :: 24
COUNT :: eng.MAX_SPRITES
COLS :: 16
FRAME_LOG_EVERY :: 60
main :: proc() {
app: eng.App
@@ -16,25 +19,45 @@ main :: proc() {
sprites: [COUNT]eng.Sprite
for i in 0 ..< COUNT {
col := i % 8
row := i / 8
col := i % COLS
row := i / COLS
pos := eng.Vec2 {
f32(120 + col * 80),
f32(280 + row * 120),
f32(40 + col * 48),
f32(80 + row * 60),
}
clip := "idle" if (i % 2) == 0 else "walk"
sprites[i] = eng.spawn_sprite(&data, pos, clip, i % 5)
}
// Look at the middle of the grid
app.camera.position = {400, 400}
last := eng.now_seconds()
frame_i := 0
sum_ms: f64
peak_ms: f64
for eng.events() {
now := eng.now_seconds()
dt := f32(now - last)
last = now
frame_ms := f64(dt) * 1000.0
sum_ms += frame_ms
if frame_ms > peak_ms do peak_ms = frame_ms
frame_i += 1
if frame_i % FRAME_LOG_EVERY == 0 {
avg := sum_ms / f64(FRAME_LOG_EVERY)
fps := 1000.0 / avg if avg > 0 else 0
fmt.printfln(
"crowd frame: avg=%.2f ms (%.1f FPS) peak=%.2f ms over %d frames",
avg,
fps,
peak_ms,
FRAME_LOG_EVERY,
)
sum_ms = 0
peak_ms = 0
}
for &s in sprites {
eng.update_sprite(&s, dt)
+79
View File
@@ -0,0 +1,79 @@
package main
import "core:fmt"
import eng "pkg:engine"
// Multi-texture batching stress: two Character_Data (two GPU textures), alternating sprites.
COUNT :: eng.MAX_SPRITES
COLS :: 16
FRAME_LOG_EVERY :: 60
BATCH_GROUP :: u32(1)
TOAD_JSON :: "assets_baked/characters/toad/toad.char.json"
main :: proc() {
app: eng.App
if !eng.init(&app, "crowd_batch", 800, 600) do return
defer eng.shutdown(&app)
data_a, ok_a := eng.load_character_data(&app, TOAD_JSON)
if !ok_a do return
defer eng.destroy_character_data(&app, &data_a)
data_b, ok_b := eng.load_character_data(&app, TOAD_JSON)
if !ok_b do return
defer eng.destroy_character_data(&app, &data_b)
sprites: [COUNT]eng.Sprite
for i in 0 ..< COUNT {
col := i % COLS
row := i / COLS
pos := eng.Vec2 {
f32(40 + col * 48),
f32(80 + row * 60),
}
clip := "idle" if (i % 2) == 0 else "walk"
data := &data_a if (i % 2) == 0 else &data_b
sprites[i] = eng.spawn_sprite(data, pos, clip, i % 5)
}
app.camera.position = {400, 400}
last := eng.now_seconds()
frame_i := 0
sum_ms: f64
peak_ms: f64
for eng.events() {
now := eng.now_seconds()
dt := f32(now - last)
last = now
frame_ms := f64(dt) * 1000.0
sum_ms += frame_ms
if frame_ms > peak_ms do peak_ms = frame_ms
frame_i += 1
if frame_i % FRAME_LOG_EVERY == 0 {
avg := sum_ms / f64(FRAME_LOG_EVERY)
fps := 1000.0 / avg if avg > 0 else 0
fmt.printfln(
"crowd_batch frame: avg=%.2f ms (%.1f FPS) peak=%.2f ms over %d frames",
avg,
fps,
peak_ms,
FRAME_LOG_EVERY,
)
sum_ms = 0
peak_ms = 0
}
for &s in sprites {
eng.update_sprite(&s, dt)
}
eng.begin_frame(&app)
for &s in sprites {
eng.draw_sprite_batched(&app, &s, BATCH_GROUP)
}
eng.end_frame(&app)
}
}
+70
View File
@@ -0,0 +1,70 @@
package main
import "core:fmt"
import eng "pkg:engine"
// GPU overdraw stress: MAX_SPRITES stacked near one world point.
COUNT :: eng.MAX_SPRITES
FRAME_LOG_EVERY :: 60
main :: proc() {
app: eng.App
if !eng.init(&app, "crowd_overdraw", 800, 600) do return
defer eng.shutdown(&app)
data, ok := eng.load_character_data(&app, "assets_baked/characters/toad/toad.char.json")
if !ok do return
defer eng.destroy_character_data(&app, &data)
center := eng.Vec2{400, 400}
sprites: [COUNT]eng.Sprite
for i in 0 ..< COUNT {
jitter := eng.Vec2 {
f32((i % 7) - 3),
f32((i % 5) - 2),
}
clip := "idle" if (i % 2) == 0 else "walk"
sprites[i] = eng.spawn_sprite(&data, center + jitter, clip, i % 5)
}
app.camera.position = {400, 400}
last := eng.now_seconds()
frame_i := 0
sum_ms: f64
peak_ms: f64
for eng.events() {
now := eng.now_seconds()
dt := f32(now - last)
last = now
frame_ms := f64(dt) * 1000.0
sum_ms += frame_ms
if frame_ms > peak_ms do peak_ms = frame_ms
frame_i += 1
if frame_i % FRAME_LOG_EVERY == 0 {
avg := sum_ms / f64(FRAME_LOG_EVERY)
fps := 1000.0 / avg if avg > 0 else 0
fmt.printfln(
"crowd_overdraw frame: avg=%.2f ms (%.1f FPS) peak=%.2f ms over %d frames",
avg,
fps,
peak_ms,
FRAME_LOG_EVERY,
)
sum_ms = 0
peak_ms = 0
}
for &s in sprites {
eng.update_sprite(&s, dt)
}
eng.begin_frame(&app)
for &s in sprites {
eng.draw_sprite(&app, &s)
}
eng.end_frame(&app)
}
}
+783
View File
@@ -0,0 +1,783 @@
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 1K of event 'cpu/cycles/Pu'
# Event count (approx.): 8251313686
#
# Overhead Command Shared Object Symbol
# ........ ............... .............................. ......................................................................................................................................................................
#
10.24% crowd_perf crowd_perf [.] engine::draw_sprite
|
---engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
5.73% crowd_perf crowd_perf [.] engine::to_clip
|
---engine::to_clip
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
2.74% crowd_perf crowd_perf [.] runtime::default_hasher
|
---runtime::default_hasher
runtime::default_hasher_string
__$hasher$$string
|
|--2.10%--engine::character_clip
| engine::update_sprite
| main::main
| main
| 0x7fec32c27740
| __libc_start_main
| _start
|
--0.64%--engine::character_frame
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
2.48% crowd_perf crowd_perf [.] engine::sprite_feet_quad
|
---engine::sprite_feet_quad
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
2.16% crowd_perf libc.so.6 [.] pthread_mutex_lock
|
---pthread_mutex_lock
|
--0.60%--wl_proxy_marshal_array_flags
wl_proxy_marshal_flags
2.09% crowd_perf crowd_perf [.] __$map_get$$map[string]engine::Clip_Def
|
---__$map_get$$map[string]engine::Clip_Def
|
|--1.49%--engine::character_clip
| engine::update_sprite
| main::main
| main
| 0x7fec32c27740
| __libc_start_main
| _start
|
--0.60%--engine::character_frame
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
2.04% crowd_perf crowd_perf [.] engine::update_sprite
|
---engine::update_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
1.91% crowd_perf crowd_perf [.] runtime::memory_equal
|
---runtime::memory_equal
runtime::string_eq
__$map_get$$map[string]engine::Clip_Def
|
|--1.32%--engine::character_frame
| engine::draw_sprite
| main::main
| main
| 0x7fec32c27740
| __libc_start_main
| _start
|
--0.58%--engine::character_clip
engine::update_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
1.90% crowd_perf crowd_perf [.] engine::character_clip
|
---engine::character_clip
engine::update_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
1.85% crowd_perf crowd_perf [.] engine::world_to_screen
|
---engine::world_to_screen
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
1.41% crowd_perf crowd_perf [.] runtime::_append_elem
|
---runtime::_append_elem
runtime::append_elem:proc(array:^[dynamic]engine::Queued_Sprite,arg:engine::Queued_Sprite,loc:runtime::Source_Code_Location)->(n:int,err:runtime::Allocator_Error)
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
1.37% crowd_perf crowd_perf [.] engine::end_frame
|
---engine::end_frame
main::main
main
0x7fec32c27740
__libc_start_main
_start
1.15% crowd_perf libc.so.6 [.] clock_gettime
|
---clock_gettime
1.05% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_atomic_dec
|
---_dbus_atomic_dec
|
--0.95%--_dbus_connection_unref_unlocked
0x7fec32e8fbae
0x7fec32e8fdad
0x7fec32e94b31
0x7fec33054fca
0x7fec33055bbd
engine::events
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.95% crowd_perf crowd_perf [.] main::main
|
---main::main
main
0x7fec32c27740
__libc_start_main
_start
0.91% crowd_perf crowd_perf [.] runtime::append_elem:proc(array:^[dynamic]engine::Queued_Sprite,arg:engine::Queued_Sprite,loc:runtime::Source_Code_Location)->(n:int,err:runtime::Allocator_Error)
|
---runtime::append_elem:proc(array:^[dynamic]engine::Queued_Sprite,arg:engine::Queued_Sprite,loc:runtime::Source_Code_Location)->(n:int,err:runtime::Allocator_Error)
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.91% crowd_perf libc.so.6 [.] 0x000000000018a4c4
|
---0x7fec32d8a4c4
|
--0.82%--engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.86% crowd_perf crowd_perf [.] runtime::string_eq
|
---runtime::string_eq
__$map_get$$map[string]engine::Clip_Def
|
--0.60%--engine::character_clip
engine::update_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.84% crowd_perf crowd_perf [.] engine::character_frame
|
---engine::character_frame
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.81% crowd_perf libwayland-client.so.0.25.0 [.] wl_proxy_marshal_array_flags
|
---wl_proxy_marshal_array_flags
wl_proxy_marshal_flags
0.77% crowd_perf libc.so.6 [.] 0x00000000000a7159
|
---0x7fec32ca7159
|
--0.61%--0x7fec12d80142
0x7fec12d82613
0x7fec12d3cff4
0x7fec12ddcaf6
0x7fec12ddccd4
0x7fec12de6f2c
0x7fec3324fec1
engine::end_frame
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.74% wl_cursor_surfa libc.so.6 [.] 0x0000000000093fff
0.73% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000143ffe
|
---0x7fec33143ffe
0x7fec3314a028
0x7fec33141b9a
0.73% crowd_perf crowd_perf [.] runtime::bounds_check_error
|
---runtime::bounds_check_error
|
--0.53%--engine::character_frame
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.71% crowd_perf libvulkan_radeon.so [.] 0x00000000001f41b7
|
---0x7fec12df41b7
0x7fec12df44e5
|
--0.59%--0x7fec12c2baf6
0x7fec12c2da03
0x7fec12c2e4ca
0x7fec12c2f277
0x7fec12de052c
0x7fec12de1521
0x7fec33247581
0x7fec33064bab
engine::end_frame
main::main
0.70% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023af6a
|
---0x7fec3323af6a
|
--0.58%--0x7fec33249f38
engine::end_frame
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.69% crowd_perf crowd_perf [.] engine::frame_uvs
|
---engine::frame_uvs
engine::draw_sprite
main::main
main
0x7fec32c27740
__libc_start_main
_start
0.68% crowd_perf libc.so.6 [.] 0x000000000009ca5c
|
---0x7fec32c9ca5c
0.67% crowd_perf [vdso] [.] __vdso_clock_gettime
|
---__vdso_clock_gettime
clock_gettime
0.58% crowd_perf libdrm.so.2.134.0 [.] drmIoctl
0.58% crowd_perf libvulkan_radeon.so [.] 0x000000000033b80b
|
---0x7fec12f3b80b
0x7fec12df41ce
0x7fec12df44e5
0.53% crowd_perf libc.so.6 [.] cfree
|
---cfree
0.51% crowd_perf [unknown] [k] 0xffffffff84e00080
|
---0xffffffff84e00080
0.45% crowd_perf libc.so.6 [.] pthread_mutex_unlock
0.44% crowd_perf libc.so.6 [.] pthread_rwlock_rdlock
0.43% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2c23
0.40% crowd_perf libc.so.6 [.] 0x0000000000180625
0.39% crowd_perf libc.so.6 [.] 0x000000000018a547
0.37% crowd_perf crowd_perf [.] runtime::default_hasher_string
0.36% crowd_perf libc.so.6 [.] 0x000000000018a4c0
0.35% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000247484
0.33% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003147
0.33% crowd_perf libc.so.6 [.] 0x0000000000189cc4
0.33% crowd_perf [unknown] [k] 0xffffffff84e00084
0.31% wl_cursor_surfa libc.so.6 [.] 0x00000000000a44b7
0.31% crowd_perf libvulkan_radeon.so [.] 0x0000000000110d98
0.31% wl_cursor_surfa libc.so.6 [.] 0x000000000018a4f3
0.30% crowd_perf libc.so.6 [.] 0x000000000018a4e2
0.30% crowd_perf libvulkan_radeon.so [.] 0x00000000000e6624
0.28% crowd_perf libvulkan_radeon.so [.] 0x000000000033b84d
0.28% crowd_perf libc.so.6 [.] 0x00000000000a4524
0.28% crowd_perf libc.so.6 [.] malloc
0.27% crowd_perf crowd_perf [.] runtime::map_seed_from_map_data
0.27% crowd_perf libvulkan_radeon.so [.] 0x00000000001822f7
0.26% crowd_perf libvulkan_radeon.so [.] 0x000000000033c9d7
0.26% crowd_perf libvulkan_radeon.so [.] 0x00000000000be362
0.26% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024cff0
0.26% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd8a8
0.26% crowd_perf libvulkan_radeon.so [.] 0x000000000018d44f
0.25% crowd_perf libvulkan_radeon.so [.] 0x00000000001d131e
0.25% crowd_perf libvulkan_radeon.so [.] 0x0000000000116255
0.25% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_atomic_inc
0.25% crowd_perf crowd_perf [.] __$hasher$$string
0.25% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000062360
0.25% crowd_perf libdrm.so.2.134.0 [.] drmSyncobjTimelineWait
0.24% crowd_perf libvulkan_radeon.so [.] 0x0000000000022daa
0.24% crowd_perf libc.so.6 [.] 0x000000000018a486
0.24% crowd_perf libvulkan_radeon.so [.] 0x00000000000b7b5f
0.24% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_message_loader_queue_messages
0.24% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000168f4
0.24% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000144008
0.24% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024cd09
0.23% crowd_perf libc.so.6 [.] 0x00000000000a45c9
0.23% crowd_perf crowd_perf [.] engine::texture_run_len
0.23% crowd_perf [unknown] [k] 0xffffffff86355a81
0.23% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023af07
0.23% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024d030
0.23% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003140
0.23% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000166c5
0.22% crowd_perf libc.so.6 [.] pthread_rwlock_unlock
0.22% crowd_perf libc.so.6 [.] 0x000000000018a4ea
0.22% crowd_perf libc.so.6 [.] 0x000000000018a4ca
0.22% crowd_perf libc.so.6 [.] 0x000000000009ca36
0.22% crowd_perf crowd_perf [.] engine::begin_frame
0.22% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002501f2
0.22% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000032b2
0.22% crowd_perf libvulkan_radeon.so [.] 0x00000000000cf9cf
0.22% crowd_perf libc.so.6 [.] 0x00000000000a58f9
0.22% crowd_perf libvulkan_radeon.so [.] 0x00000000001e47ac
0.21% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003263
0.21% crowd_perf libvulkan_radeon.so [.] 0x00000000001e61b8
0.21% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000049dc
0.21% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001cd7b4
0.21% crowd_perf [unknown] [k] 0xffffffff85a7a084
0.20% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2761
0.19% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003203
0.19% crowd_perf libvulkan_radeon.so [.] 0x000000000002295c
0.19% crowd_perf libc.so.6 [.] 0x0000000000190598
0.18% crowd_perf libvulkan_radeon.so [.] 0x00000000000d33ea
0.17% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000541ae
0.17% crowd_perf [unknown] [k] 0xffffffff855d7847
0.17% crowd_perf libvulkan_radeon.so [.] 0x000000000037a095
0.17% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5b8f
0.17% crowd_perf libdbus-1.so.3.38.3 [.] 0x0000000000016a5d
0.17% crowd_perf libvulkan_radeon.so [.] 0x000000000002ca02
0.16% crowd_perf libvulkan_radeon.so [.] 0x0000000000181459
0.16% wl_cursor_surfa libwayland-client.so.0.25.0 [.] wl_display_flush
0.16% crowd_perf libvulkan_radeon.so [.] 0x00000000000ebd77
0.16% crowd_perf libvulkan_radeon.so [.] 0x00000000001e35cb
0.16% crowd_perf libdbus-1.so.3.38.3 [.] 0x0000000000031bdf
0.16% crowd_perf libvulkan_radeon.so [.] 0x00000000000bbf75
0.16% crowd_perf libvulkan_radeon.so [.] 0x00000000001e4a6a
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000001e4d24
0.15% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000016610
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000ed46e
0.15% crowd_perf libvulkan_radeon.so [.] 0x000000000018d496
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000b8dea
0.15% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024fd04
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000249f10
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000ba831
0.14% crowd_perf libc.so.6 [.] 0x000000000018a55c
0.14% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003165
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000035f660
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000115185
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000018584
0.14% crowd_perf libc.so.6 [.] 0x000000000009ffc4
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000657d0
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000028528
0.14% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000004cb4
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000021b325
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000013c5ff
0.14% wl_cursor_surfa libc.so.6 [.] 0x00000000000a7586
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000016675
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000035f6d0
0.14% crowd_perf [unknown] [k] 0xffffffff855e8fd6
0.14% crowd_perf libc.so.6 [.] 0x00000000000a44b7
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000018aae8
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000022b11
0.14% crowd_perf libdbus-1.so.3.38.3 [.] dbus_connection_ref
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001d9f6f
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001814d7
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000035f634
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000181ee4
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000033cb4a
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000cb17f
0.13% wl_cursor_surfa libwayland-client.so.0.25.0 [.] wl_display_read_events
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000018c7aa
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000033b7e1
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000c9cb4
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5e16
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000017dbbb
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000f7a33
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000017ea01
0.13% crowd_perf libnvidia-glcore.so.610.43.03 [.] 0x0000000000fcd46c
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000ccf75
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000002c934
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000185a9
0.13% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003193
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000115378
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000062cee
0.13% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000039f1
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5525
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000001dbca4
0.13% wl_cursor_surfa libc.so.6 [.] 0x0000000000094084
0.13% wl_cursor_surfa libc.so.6 [.] 0x00000000000a66c4
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000001c8cfd
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5b0f
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000116284
0.13% crowd_perf libwayland-client.so.0.25.0 [.] 0x000000000000320b
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000017d7c4
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023af40
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000002cd72
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd651
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000217945
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024e846
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000017ff51
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000186261
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002440cf
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000ebdf7
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001f3ff0
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cb17b
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001deb23
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2e1b
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001f44d1
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001161e8
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000d88e7
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001168fc
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cea95
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001e4771
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000336cae
0.12% crowd_perf [unknown] [k] 0xffffffff84e00087
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd7c4
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000b8712
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000010da71
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001dca18
0.12% crowd_perf libc.so.6 [.] 0x00000000000a6ed3
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000bf533
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000ceb86
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001166e5
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000068cca2
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000116312
0.12% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_list_pop_first_link
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000180ab2
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000002336f
0.12% crowd_perf crowd_perf [.] engine::now_seconds
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024fc98
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001f3ff4
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000116118
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000181449
0.12% crowd_perf libdrm.so.2.134.0 [.] drmSyncobjWait
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000b707d
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000011621a
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023c102
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002469b4
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001d8d26
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000bdcf2
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001e4c57
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000c2897
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023be6f
0.12% crowd_perf [unknown] [k] 0xffffffff84e015f4
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000025442a
0.12% crowd_perf libc.so.6 [.] 0x00000000000a6403
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001f3010
0.12% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001e3f78
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001e61f1
0.12% crowd_perf [unknown] [k] 0xffffffff85a7a016
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000018d4e4
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000f4af3
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001153f9
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cb4b9
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000283b6
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2d0b
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001e6d70
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000115a48
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024d110
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000017ff7a
0.11% crowd_perf libc.so.6 [.] 0x00000000000a5735
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000011416b
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023aefa
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd413
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000068cc64
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000022d88
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000025478f
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000545e3
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000018c87b
0.11% crowd_perf libc.so.6 [.] 0x000000000005bcfd
0.11% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000032a4
0.11% crowd_perf libdbus-1.so.3.38.3 [.] 0x000000000001aa7b
0.11% wl_cursor_surfa libc.so.6 [.] 0x00000000000a674c
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000d6749
0.11% crowd_perf libffi.so.8.4.1 [.] 0x0000000000002bf4
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000054e93
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000018567
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000189a35
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000182313
0.11% crowd_perf libdbus-1.so.3.38.3 [.] 0x000000000003219d
0.11% crowd_perf libSDL3.so.0.4.12 [.] SDL_UnmapGPUTransferBuffer
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000176c2
0.11% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x0000000000003689
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000022a03
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000002f38d
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2686
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000244358
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000ce843
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001f41d6
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000217e57
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000cca9b
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000022d261
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000ebc8d
0.11% crowd_perf libc.so.6 [.] pthread_getspecific
0.11% crowd_perf libc.so.6 [.] 0x00000000000a4520
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000c0133
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000176d1
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002475f1
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000035f6c7
0.11% wl_cursor_surfa libc.so.6 [.] 0x00000000000a661d
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000246fbc
0.11% wl_cursor_surfa libc.so.6 [.] pthread_mutex_lock
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000c4a78
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000d227c
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000cf034
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000016e32
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000b91fb
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001e6228
0.11% crowd_perf libc.so.6 [.] 0x0000000000189c5a
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000b7a68
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2b1b
0.11% crowd_perf libc.so.6 [.] __libc_calloc
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5ba8
0.11% crowd_perf libc.so.6 [.] 0x0000000000189c6e
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000033c9cd
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000bc661
0.10% crowd_perf libvulkan_radeon.so [.] 0x0000000000022a90
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2ed4
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000cfbdd
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000001158db
0.10% crowd_perf libwayland-client.so.0.25.0 [.] 0x000000000000315e
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5b36
0.10% crowd_perf libwayland-client.so.0.25.0 [.] 0x000000000000326e
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000013c87e
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000062c29
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024e2b6
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023be24
0.10% crowd_perf libvulkan_radeon.so [.] 0x0000000000197523
0.10% crowd_perf libwayland-client.so.0.25.0 [.] wl_display_prepare_read_queue
0.10% crowd_perf libc.so.6 [.] 0x00000000000a57e4
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002441e6
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd71c
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000018c8b9
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000001cfb14
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000067a27
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000022ddf5
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000254752
0.10% crowd_perf libc.so.6 [.] 0x000000000009ca4f
0.10% crowd_perf libvulkan_radeon.so [.] 0x0000000000189e7f
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2d44
0.10% crowd_perf libc.so.6 [.] 0x0000000000183444
0.10% crowd_perf libc.so.6 [.] ioctl
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000001662f
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000d3c1f
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000ed479
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000001e4e14
0.10% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001e3f76
0.10% crowd_perf libwayland-client.so.0.25.0 [.] wl_list_empty
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000d6c8b
0.10% crowd_perf libvulkan_radeon.so [.] 0x0000000000193681
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000001dd4e5
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000018d452
0.10% crowd_perf libvulkan_radeon.so [.] 0x0000000000028474
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000bc657
0.10% crowd_perf libdbus-1.so.3.38.3 [.] dbus_connection_get_dispatch_status
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000068fe91
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000017e9d8
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000fa720
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000013d041
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000033b851
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000001e3616
0.09% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024f5a2
0.09% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000025445f
0.09% crowd_perf libvulkan_radeon.so [.] 0x00000000000cb359
0.09% crowd_perf libc.so.6 [.] strchrnul@plt
0.09% crowd_perf libvulkan_radeon.so [.] 0x00000000000d3493
0.09% crowd_perf libvulkan_radeon.so [.] 0x0000000000022b67
0.09% crowd_perf libvulkan_radeon.so [.] 0x0000000000116982
0.09% crowd_perf libvulkan_radeon.so [.] 0x000000000035f664
0.09% crowd_perf libc.so.6 [.] 0x0000000000189c47
0.08% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000031c3
0.08% crowd_perf libc.so.6 [.] 0x00000000000a66c4
0.08% crowd_perf libvulkan_radeon.so [.] 0x00000000000ebd95
0.08% crowd_perf libc.so.6 [.] 0x00000000000a5750
0.08% crowd_perf libvulkan_radeon.so [.] 0x00000000000ce980
0.08% crowd_perf libvulkan_radeon.so [.] 0x0000000000115f93
0.08% crowd_perf libvulkan_radeon.so [.] 0x000000000018d5e9
0.08% wl_cursor_surfa libc.so.6 [.] 0x000000000009ca5c
0.07% crowd_perf libvulkan_radeon.so [.] 0x000000000018aaf9
0.07% crowd_perf libvulkan_radeon.so [.] 0x00000000000c4a6a
0.07% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001cd7b1
0.07% crowd_perf libvulkan_radeon.so [.] 0x00000000001e4e3e
0.06% wl_cursor_surfa libwayland-client.so.0.25.0 [.] wl_list_insert
0.06% wl_cursor_surfa libc.so.6 [.] 0x00000000000a6ed3
0.03% crowd_perf libxkbcommon.so.0.13.2 [.] 0x00000000000084b7
0.03% wl_cursor_surfa libc.so.6 [.] 0x00000000000a6f67
0.02% crowd_perf libc.so.6 [.] 0x0000000000189d47
0.02% crowd_perf libvulkan_radeon.so [.] 0x00000000001e5074
0.02% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000ac3a
0.01% crowd_perf libvulkan_radeon.so [.] 0x0000000000182d8f
0.00% crowd_perf libc.so.6 [.] __cxa_finalize
0.00% wl_cursor_surfa libc.so.6 [.] pthread_cond_broadcast
0.00% wl_cursor_surfa [unknown] [k] 0xffffffff84e00080
0.00% crowd_perf libc.so.6 [.] 0x00000000000a7681
0.00% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x00000000000036ef
0.00% crowd_p:disk$0 libc.so.6 [.] pthread_cond_wait
0.00% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x0000000000003621
0.00% crowd_perf libc.so.6 [.] 0x00000000000534eb
0.00% crowd_perf libdbus-1.so.3.38.3 [.] 0x0000000000037c44
0.00% wl_cursor_surfa libc.so.6 [.] 0x000000000009ca21
0.00% wl_cursor_surfa libc.so.6 [.] ppoll
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000002d5ba
0.00% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000670f4
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000037a011
0.00% crowd_perf libc.so.6 [.] 0x0000000000189cf7
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x0000000000015951
0.00% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024e620
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2f92
0.00% wl_cursor_surfa libc.so.6 [.] 0x00000000000a4463
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000001e56c
0.00% crowd_perf libdbus-1.so.3.38.3 [.] 0x000000000001004a
0.00% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x00000000000031c3
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x0000000000014887
0.00% crowd_perf libc.so.6 [.] 0x000000000005b734
0.00% crowd_perf libc.so.6 [.] 0x000000000005bd50
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000001e3e38
0.00% crowd_perf libc.so.6 [.] 0x0000000000064b99
0.00% crowd_perf libc.so.6 [.] 0x000000000005a244
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345b18
0.00% crowd_perf libc.so.6 [.] __ctype_init
0.00% crowd_perf [unknown] [k] 0xffffffff86355a14
0.00% crowd_perf [unknown] [k] 0xffffffff86355a10
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345ce7
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345adb
0.00% crowd_perf libnvidia-gpucomp.so.610.43.03 [.] 0x0000000000654f48
0.00% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001e3f74
0.00% crowd_perf libc.so.6 [.] __errno_location
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000000ca8f2
0.00% crowd_perf libnvidia-gpucomp.so.610.43.03 [.] 0x0000000001961f52
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345837
0.00% crowd_perf libc.so.6 [.] pthread_self
0.00% wl_cursor_surfa [unknown] [k] 0xffffffff852a9115
0.00% crowd_perf libc.so.6 [.] 0x0000000000103a63
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000018d575
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345cfe
0.00% crowd_perf libc.so.6 [.] 0x0000000000097417
0.00% crowd_perf libc.so.6 [.] 0x000000000009741b
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000001166ae
0.00% crowd_perf [unknown] [k] 0xffffffff84e015f0
0.00% crowd_perf libwayland-client.so.0.25.0 [.] wl_display_dispatch_queue_timeout
0.00% crowd_perf libc.so.6 [.] 0x0000000000185400
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x0000000000008b5e
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000022edc
0.00% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000018892
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000028ed39
0.00% wl_cursor_surfa libc.so.6 [.] 0x0000000000094040
0.00% crowd_perf libc.so.6 [.] sendmsg
0.00% crowd_perf [unknown] [k] 0xffffffff84e01280
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000344ddb
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345bb3
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000344ddd
0.00% crowd_perf libc.so.6 [.] 0x000000000011bed0
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000344cd4
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345ad6
0.00% crowd_perf libc.so.6 [.] 0x0000000000094047
0.00% crowd_perf libc.so.6 [.] 0x00000000000973a0
0.00% crowd_perf libc.so.6 [.] 0x0000000000103a40
0.00% wl_cursor_surfa libc.so.6 [.] 0x0000000000094047
0.00% crowd_perf libc.so.6 [.] __poll
0.00% crowd_perf libc.so.6 [.] 0x00000000000973a4
0.00% crowd_perf libdbus-1.so.3.38.3 [.] 0x0000000000032197
0.00% crowd_perf [unknown] [k] 0xffffffff84e01630
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000932e
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x0000000000009339
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000001f103
0.00% crowd_perf libc.so.6 [.] ppoll
0.00% crowd_perf libc.so.6 [.] realloc
0.00% crowd_perf libc.so.6 [.] 0x0000000000094040
0.00% crowd_perf libc.so.6 [.] 0x0000000000094084
0.00% crowd_perf libc.so.6 [.] 0x00000000000a78dc
0.00% crowd_perf libnvidia-glcore.so.610.43.03 [.] 0x00000000009e786b
0.00% crowd_perf libnvidia-gpucomp.so.610.43.03 [.] 0x0000000000455dd3
0.00% crowd_perf libnvidia-gpucomp.so.610.43.03 [.] 0x000000000067d1b0
0.00% crowd_perf libnvidia-gpucomp.so.610.43.03 [.] 0x000000000067d1c2
0.00% crowd_perf libnvidia-gpucomp.so.610.43.03 [.] 0x0000000001956940
0.00% crowd_perf libnvidia-gpucomp.so.610.43.03 [.] 0x000000000195a141
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000022e9e
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000022ea7
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000022ebd
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000000bafc5
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000000ca918
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000000ca91f
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000011666f
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000001166a8
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2dbf
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2dd0
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000028ece4
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000028ed0d
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345bb0
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000068d01a
0.00% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003aa8
0.00% crowd_perf [unknown] [k] 0xffffffff84e01670
0.00% crowd_perf [unknown] [k] 0xffffffff84e01690
0.00% wl_cursor_surfa libc.so.6 [.] 0x0000000000094050
0.00% wl_cursor_surfa libc.so.6 [.] 0x0000000000094085
0.00% wl_cursor_surfa libc.so.6 [.] 0x000000000009fff2
0.00% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x0000000000004524
0.00% wl_cursor_surfa [unknown] [k] 0xffffffff84e01630
#
# (Tip: For tracepoint events, try: perf report -s trace_fields)
#
Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 82 KiB

+730
View File
@@ -0,0 +1,730 @@
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 953 of event 'cpu/cycles/Pu'
# Event count (approx.): 5763089468
#
# Overhead Command Shared Object Symbol
# ........ ............... ............................. ..........................................................................................................................................................................................
#
4.03% crowd_perf libc.so.6 [.] pthread_mutex_lock
|
---pthread_mutex_lock
|
--0.65%--0x7f80850501f1
engine::end_frame (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
3.19% crowd_perf crowd_perf [.] main::main
|
--0.79%--runtime::default_hasher (inlined)
runtime::default_hasher_string (inlined)
|
--0.54%--engine::character_clip (inlined)
engine::update_sprite (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
2.85% crowd_perf libc.so.6 [.] 0x00000000000a73e9
|
---0x7f8084aa73e9
wl_proxy_marshal_array_flags
wl_proxy_marshal_flags
0x7f8082b97614
0x7f8082b8a4f8
0x7f808504ffc9
engine::end_frame (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
2.20% crowd_perf crowd_perf [.] __$map_get$$map[string]engine::Clip_Def
|
---__$map_get$$map[string]engine::Clip_Def
|
--1.91%--engine::character_clip (inlined)
engine::update_sprite (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
2.07% crowd_perf libc.so.6 [.] 0x0000000000189c53
|
---0x7f8084b89c53
runtime::_append_elem
runtime::append_elem:proc(array:^[dynamic]engine::Queued_Sprite,arg:engine::Queued_Sprite,loc:runtime::Source_Code_Location)->(n:int,err:runtime::Allocator_Error) (inlined)
engine::draw_sprite (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
1.83% crowd_perf libc.so.6 [.] clock_gettime
|
---clock_gettime
|
|--0.53%--0x7f808502c819
|
--0.52%--0x7f8082d7a09a
0x7f8082d3de27
|
--0.52%--0x7f8082b975b5
0x7f8082b8a4f8
0x7f808504ffc9
engine::end_frame (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
1.58% crowd_perf libwayland-client.so.0.25.0 [.] wl_proxy_marshal_array_flags
|
---wl_proxy_marshal_array_flags
wl_proxy_marshal_flags
1.55% crowd_perf libvulkan_radeon.so [.] 0x000000000031fb52
|
---0x7f8082d1fb52
0x7f8082a2cc72
0x7f8082a2cf4e
0x7f8082a2e299
0x7f8082a2f277
0x7f8082be052c
0x7f8082be1521
0x7f8085047581
0x7f8084e64bab
engine::end_frame (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
1.34% wl_cursor_surfa libwayland-client.so.0.25.0 [.] wl_list_insert
1.30% wl_cursor_surfa libc.so.6 [.] 0x00000000000a678f
1.18% crowd_perf [unknown] [k] 0xffffffff84e00080
|
---0xffffffff84e00080
|
--0.94%--ioctl
|
--0.80%--drmIoctl
1.07% crowd_perf libc.so.6 [.] ioctl
|
---ioctl
|
--1.07%--drmIoctl
|
--0.61%--drmSyncobjTimelineWait
|
--0.50%--0x7f8082b8d8ad
0x7f8082b93f45
0x7f8082b89296
0x7f8082b89241
0x7f80850544bd
0x7f8084e69860
engine::begin_frame (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
1.05% crowd_perf [vdso] [.] __vdso_clock_gettime
|
---__vdso_clock_gettime
clock_gettime
|
--0.50%--0x7f8082d7a09a
0.93% crowd_perf [unknown] [k] 0xffffffff86355a81
|
---0xffffffff86355a81
0xffffffff84e0012f
|
--0.81%--ioctl
|
--0.69%--drmIoctl
0.92% crowd_perf libdrm.so.2.134.0 [.] drmIoctl
|
---drmIoctl
0.85% crowd_perf libc.so.6 [.] cfree
|
---cfree
0.81% crowd_perf libc.so.6 [.] 0x000000000009ca5c
|
---0x7f8084a9ca5c
0.78% crowd_perf libc.so.6 [.] 0x00000000000a6ed3
|
---0x7f8084aa6ed3
|
--0.55%--0x7f8084aa714d
0.70% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002501f2
|
---0x7f80850501f2
engine::end_frame (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
0.65% crowd_perf libvulkan_radeon.so [.] 0x0000000000022d88
|
---0x7f8082a22d88
0.62% crowd_perf libvulkan_radeon.so [.] 0x00000000001f41b7
|
---0x7f8082bf41b7
0x7f8082bf44e5
0.61% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2c23
|
---0x7f8082ad2c23
0x7f8082be6325
0.60% crowd_perf libc.so.6 [.] 0x000000000018a547
|
---0x7f8084b8a547
0.56% crowd_perf libc.so.6 [.] malloc
|
---malloc
0.55% crowd_perf libvulkan_radeon.so [.] 0x00000000000e6624
|
---0x7f8082ae6624
0x7f8082be6325
0.53% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002475f1
|
---0x7f80850475f1
0x7f8084e64bab
engine::end_frame (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
0.53% crowd_perf libvulkan_radeon.so [.] 0x00000000001e61b8
|
---0x7f8082be61b8
0x7f808503b9d8
0x7f8085049f22
engine::end_frame (inlined)
main::main
main
0x7f8084a27740
__libc_start_main
_start
0.51% crowd_perf [unknown] [k] 0xffffffff855d7847
|
---0xffffffff855d7847
0xffffffff86355aba
0xffffffff84e0012f
ioctl
drmIoctl
0.50% crowd_perf libvulkan_radeon.so [.] 0x000000000033b80b
|
---0x7f8082d3b80b
0x7f8082bf41ce
0.50% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023af6a
0.48% crowd_perf libvulkan_radeon.so [.] 0x00000000001deb23
0.48% crowd_perf libc.so.6 [.] pthread_rwlock_unlock
0.48% crowd_perf libvulkan_radeon.so [.] 0x000000000033c9d7
0.47% crowd_perf libvulkan_radeon.so [.] 0x000000000002ca18
0.46% wl_cursor_surfa libwayland-client.so.0.25.0 [.] wl_display_flush
0.46% crowd_perf libvulkan_radeon.so [.] 0x00000000000b92b8
0.45% crowd_perf libvulkan_radeon.so [.] 0x00000000001822f7
0.44% crowd_perf libc.so.6 [.] pthread_rwlock_wrlock
0.41% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003203
0.41% crowd_perf libc.so.6 [.] __errno_location
0.40% crowd_perf libvulkan_radeon.so [.] 0x0000000000180ab2
0.40% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024d183
0.40% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2761
0.39% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000016900
0.39% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000243e0a
0.39% wl_cursor_surfa libc.so.6 [.] 0x00000000000a6747
0.36% crowd_perf libvulkan_radeon.so [.] 0x000000000018d492
0.35% crowd_perf libvulkan_radeon.so [.] 0x00000000001814c2
0.35% crowd_perf libvulkan_radeon.so [.] 0x00000000000b8fdd
0.35% crowd_perf libvulkan_radeon.so [.] 0x00000000000e5edc
0.33% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001e3f74
0.33% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000001493ed
0.32% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000005430
0.32% crowd_perf libc.so.6 [.] 0x00000000000a7554
0.29% crowd_perf libvulkan_radeon.so [.] 0x00000000001e0534
0.29% crowd_perf libwayland-client.so.0.25.0 [.] wl_proxy_marshal_flags
0.29% crowd_perf libvulkan_radeon.so [.] 0x0000000000110d3b
0.29% crowd_perf libvulkan_radeon.so [.] 0x00000000001defd2
0.29% wl_cursor_surfa libc.so.6 [.] 0x000000000018a507
0.29% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003147
0.29% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000032a4
0.28% crowd_perf libvulkan_radeon.so [.] 0x000000000018d44f
0.28% crowd_perf libvulkan_radeon.so [.] 0x000000000018d866
0.28% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000168f4
0.27% wl_cursor_surfa libc.so.6 [.] ppoll
0.27% crowd_perf libvulkan_radeon.so [.] 0x00000000000d227c
0.27% crowd_perf crowd_perf [.] runtime::_append_elem
0.27% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003943
0.27% crowd_perf libvulkan_radeon.so [.] 0x00000000000b91fd
0.26% crowd_perf libc.so.6 [.] pthread_mutex_unlock
0.26% crowd_perf libvulkan_radeon.so [.] 0x00000000001f41d6
0.26% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002441e6
0.26% crowd_perf libc.so.6 [.] 0x00000000000a6ed7
0.26% crowd_perf libc.so.6 [.] 0x00000000000a590c
0.26% crowd_perf [unknown] [k] 0xffffffff84e00084
0.26% wl_cursor_surfa libwayland-client.so.0.25.0 [.] wl_display_read_events
0.25% crowd_perf libvulkan_radeon.so [.] 0x000000000033cb4a
0.25% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2f9e
0.25% crowd_perf libc.so.6 [.] 0x0000000000180604
0.25% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000016904
0.25% crowd_perf libvulkan_radeon.so [.] 0x0000000000181449
0.25% crowd_perf libvulkan_radeon.so [.] 0x00000000000fabbd
0.25% crowd_perf libvulkan_radeon.so [.] 0x0000000000022daa
0.25% crowd_perf libvulkan_radeon.so [.] 0x00000000000b69b6
0.24% crowd_perf libvulkan_radeon.so [.] 0x00000000000ba831
0.24% wl_cursor_surfa libc.so.6 [.] pthread_mutex_lock
0.24% crowd_perf libvulkan_radeon.so [.] 0x000000000068ccac
0.24% crowd_perf libc.so.6 [.] 0x00000000000a7101
0.24% crowd_perf libvulkan_radeon.so [.] 0x00000000001152d6
0.24% crowd_perf libvulkan_radeon.so [.] 0x00000000000bdd3d
0.24% crowd_perf libc.so.6 [.] 0x000000000018a54e
0.24% crowd_perf [unknown] [k] 0xffffffff85a7a016
0.24% wl_cursor_surfa libc.so.6 [.] recvmsg
0.23% crowd_perf libvulkan_radeon.so [.] 0x000000000035f607
0.23% crowd_perf libvulkan_radeon.so [.] 0x00000000001f3ff4
0.23% crowd_perf libdrm.so.2.134.0 [.] drmSyncobjWait
0.23% crowd_perf libwayland-client.so.0.25.0 [.] 0x000000000000320b
0.22% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023434e
0.22% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024ff8e
0.22% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002504d8
0.21% wl_cursor_surfa libc.so.6 [.] 0x0000000000093fe4
0.21% crowd_perf libvulkan_radeon.so [.] 0x00000000001f4165
0.20% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000018896
0.20% crowd_perf libc.so.6 [.] 0x0000000000183444
0.19% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd91c
0.19% crowd_perf libvulkan_radeon.so [.] 0x00000000000e6243
0.19% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd030
0.19% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000001663d
0.18% crowd_perf libvulkan_radeon.so [.] 0x0000000000115378
0.18% crowd_perf libvulkan_radeon.so [.] 0x00000000001da01c
0.18% crowd_perf libc.so.6 [.] 0x00000000000a579b
0.18% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x0000000000004cb4
0.17% crowd_perf libvulkan_radeon.so [.] 0x00000000000ebd58
0.17% crowd_perf libc.so.6 [.] 0x0000000000180625
0.17% crowd_perf libvulkan_radeon.so [.] 0x00000000001df838
0.17% crowd_perf libvulkan_radeon.so [.] 0x00000000000cfab0
0.17% crowd_perf libc.so.6 [.] 0x00000000000a678f
0.17% crowd_perf libvulkan_radeon.so [.] 0x00000000000cf034
0.16% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024cec9
0.16% crowd_perf [unknown] [k] 0xffffffff86355a14
0.16% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023af5d
0.16% wl_cursor_surfa libc.so.6 [.] 0x0000000000094085
0.16% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_atomic_inc
0.16% crowd_perf libvulkan_radeon.so [.] 0x000000000037a25e
0.16% crowd_perf libdbus-1.so.3.38.3 [.] dbus_connection_unref
0.16% crowd_perf libvulkan_radeon.so [.] 0x0000000000181484
0.16% crowd_perf libwayland-client.so.0.25.0 [.] wl_list_remove
0.16% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000025445f
0.16% crowd_perf libc.so.6 [.] 0x000000000009ca14
0.16% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_connection_lock
0.16% crowd_perf libvulkan_radeon.so [.] 0x0000000000217ee9
0.16% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000254472
0.16% crowd_perf libvulkan_radeon.so [.] 0x00000000001dd6f2
0.16% crowd_perf libvulkan_radeon.so [.] 0x00000000000f4ad9
0.16% crowd_perf libnvidia-glcore.so.610.43.03 [.] 0x0000000000fcd461
0.16% crowd_perf libvulkan_radeon.so [.] 0x000000000018aaf2
0.16% crowd_perf libvulkan_radeon.so [.] 0x000000000002caab
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000001e6434
0.15% crowd_perf libvulkan_radeon.so [.] 0x000000000002ca15
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5ba8
0.15% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003263
0.15% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000015f1c3
0.15% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000032dc
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000001f3344
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000d7063
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000c4a41
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000ec895
0.15% crowd_perf libvulkan_radeon.so [.] 0x0000000000335bd4
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000ebdc5
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000001f3ff6
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000ebe76
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000ed816
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000001e61f1
0.15% crowd_perf libvulkan_radeon.so [.] 0x000000000002d5ef
0.15% crowd_perf libc.so.6 [.] 0x000000000018062b
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000d13c3
0.15% crowd_perf libvulkan_radeon.so [.] 0x00000000000d8386
0.15% wl_cursor_surfa libc.so.6 [.] 0x000000000009ca5c
0.15% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000015f1a4
0.15% crowd_perf libc.so.6 [.] 0x00000000000a5735
0.15% crowd_perf [unknown] [k] 0xffffffff84e00087
0.15% crowd_perf libvulkan_radeon.so [.] 0x0000000000022b88
0.15% crowd_perf libvulkan_radeon.so [.] 0x0000000000217dba
0.15% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000001da04c
0.15% crowd_perf libvulkan_radeon.so [.] 0x000000000019371b
0.15% crowd_perf libc.so.6 [.] 0x0000000000189c44
0.14% crowd_perf libdbus-1.so.3.38.3 [.] 0x000000000001ab32
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000196829
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000017690
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000018d844
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000bddf1
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000013ca2e
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000054e60
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000194631
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000c9aeb
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000017dbc1
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000197523
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001cf344
0.14% crowd_perf libdbus-1.so.3.38.3 [.] 0x000000000001aa6d
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024ce11
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000055379
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024bb5e
0.14% crowd_perf libdbus-1.so.3.38.3 [.] 0x0000000000031883
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001d6418
0.14% crowd_perf libc.so.6 [.] 0x000000000018a57d
0.14% crowd_perf libc.so.6 [.] 0x0000000000189c93
0.14% crowd_perf libc.so.6 [.] 0x000000000009ca4f
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000168f0
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000028474
0.14% crowd_perf libc.so.6 [.] 0x00000000000a7477
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000022a86
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000018d4ac
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000035f6c7
0.14% crowd_perf libc.so.6 [.] 0x00000000000a66c4
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000002310fb
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000176c6
0.14% crowd_perf libc.so.6 [.] 0x000000000009ca19
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023b2d1
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000002d8b8
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001d9f47
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000bbf60
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000cea95
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000115ac7
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000115b65
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002470cf
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024d18e
0.14% crowd_perf libc.so.6 [.] 0x000000000009ffc0
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000cf4c5
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000244261
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000002d886
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001896c4
0.14% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x00000000000049dc
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000c0ac8
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000002ca2a
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000d3493
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000001e62d
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000116118
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000053d7e
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000000c877c
0.14% crowd_perf libc.so.6 [.] 0x000000000018a4f3
0.14% crowd_perf libvulkan_radeon.so [.] 0x000000000022d354
0.14% crowd_perf libvulkan_radeon.so [.] 0x0000000000022d55
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001e5ebd
0.14% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000004c80
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001f41a4
0.14% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003140
0.14% crowd_perf libvulkan_radeon.so [.] 0x00000000001e60b2
0.14% crowd_perf libc.so.6 [.] 0x000000000009ca5a
0.14% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023c133
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024d1c6
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000033ca1e
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000be09c
0.13% crowd_perf libc.so.6 [.] 0x0000000000189cc4
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000185a0
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000cc0cb
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000b74ff
0.13% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_rmutex_lock
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000bc373
0.13% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003193
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000d148b
0.13% crowd_perf libc.so.6 [.] 0x0000000000189d47
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023c3e7
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024b818
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000217eac
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000027f8e
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000022aa5
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000c9d9f
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000002f38d
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024cf70
0.13% crowd_perf [unknown] [k] 0xffffffff855e8f44
0.13% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000032b2
0.13% crowd_perf libc.so.6 [.] 0x000000000009ca31
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000005536a
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000013c684
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024fc98
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000ed0ff
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000c6ce6
0.13% crowd_perf libdbus-1.so.3.38.3 [.] 0x000000000003187f
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000001cfb65
0.13% crowd_perf libc.so.6 [.] 0x0000000000180614
0.13% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_list_pop_first_link
0.13% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000005485
0.13% crowd_perf libc.so.6 [.] __libc_calloc
0.13% crowd_perf libwayland-client.so.0.25.0 [.] 0x000000000000320e
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000001db541
0.13% crowd_perf libc.so.6 [.] 0x000000000009408f
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2f92
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5bde
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000d16a6
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000001398b4
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024f60d
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000018d5e9
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000d793a
0.13% crowd_perf libc.so.6 [.] 0x000000000018a4c4
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000001cf563
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000ccf20
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000002310dc
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000002d7a3
0.13% crowd_perf libc.so.6 [.] __close
0.13% crowd_perf libvulkan_radeon.so [.] 0x000000000035f634
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000ed45b
0.13% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2d0b
0.13% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000f7a33
0.13% crowd_perf libc.so.6 [.] 0x00000000000a7491
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000230ea5
0.13% crowd_perf libc.so.6 [.] 0x00000000000a57a6
0.13% crowd_perf libvulkan_radeon.so [.] 0x0000000000110d5e
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000068d087
0.12% crowd_perf libdbus-1.so.3.38.3 [.] 0x0000000000031820
0.12% crowd_perf libc.so.6 [.] 0x000000000009ca17
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024d030
0.12% wl_cursor_surfa libc.so.6 [.] pthread_mutex_unlock
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024e291
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2ac1
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000018c879
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000022b1c
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001f2c85
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001e34fc
0.12% crowd_perf libc.so.6 [.] ppoll
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000bcb9e
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000002334f
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000022950
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000d649e
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cfa60
0.12% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003912
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001d1145
0.12% crowd_perf libdbus-1.so.3.38.3 [.] 0x00000000000318bf
0.12% crowd_perf libwayland-client.so.0.25.0 [.] 0x00000000000049dc
0.12% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x0000000000003689
0.12% crowd_perf libc.so.6 [.] 0x00000000000a6621
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000002d844
0.12% crowd_perf [unknown] [k] 0xffffffff86355a41
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000002bc13
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000033cb5b
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000002ce05
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000014f29b
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000025478f
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000018d462
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001e6d9f
0.12% crowd_perf libc.so.6 [.] 0x00000000000a6610
0.12% crowd_perf [unknown] [k] 0xffffffff850a84c5
0.12% crowd_perf libc.so.6 [.] 0x0000000000180653
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000033ca37
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000249e81
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001dcabc
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000110ce8
0.12% crowd_perf libwayland-client.so.0.25.0 [.] 0x000000000000328a
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024cef0
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd943
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000c2584
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd7c1
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000022f3e
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000018d635
0.12% crowd_perf libc.so.6 [.] 0x000000000009ca74
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cea42
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023b143
0.12% wl_cursor_surfa libc.so.6 [.] 0x000000000018a4f3
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd71c
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000d1865
0.12% crowd_perf libc.so.6 [.] 0x000000000018a486
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001e519a
0.12% crowd_perf libc.so.6 [.] 0x00000000000a66e2
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000019678f
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000001e5134
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000002463df
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000054e93
0.12% crowd_perf libvulkan_radeon.so [.] 0x000000000068cf84
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000b6d7d
0.12% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000af32
0.12% crowd_perf libvulkan_radeon.so [.] 0x00000000000ed081
0.12% crowd_perf libvulkan_radeon.so [.] 0x0000000000218046
0.12% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000541ae
0.12% crowd_perf libdbus-1.so.3.38.3 [.] _dbus_message_loader_queue_messages
0.12% crowd_perf libwayland-client.so.0.25.0 [.] 0x000000000000546e
0.12% crowd_perf libwayland-client.so.0.25.0 [.] 0x0000000000003233
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000180ada
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000028174
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000657d0
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000ce7ac
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000250239
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000068d014
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023ba94
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000013cae3
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000cd796
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024d1b9
0.11% crowd_perf libc.so.6 [.] 0x000000000018a507
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001f3465
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000018d452
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001e6363
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000021b281
0.11% crowd_perf libc.so.6 [.] 0x00000000000a5904
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001f401a
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000001c8c05
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000d2d97
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000379fe4
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001529e1
0.11% wl_cursor_surfa libc.so.6 [.] 0x00000000000a6732
0.11% wl_cursor_surfa libwayland-client.so.0.25.0 [.] wl_display_prepare_read_queue
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024cddc
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000010da99
0.11% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x0000000000004961
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001d62b5
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000018d874
0.11% wl_cursor_surfa libc.so.6 [.] 0x00000000000a66e2
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000182733
0.11% crowd_perf libvulkan_radeon.so [.] 0x0000000000023598
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000054f23
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x00000000000176cf
0.11% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000024e2b6
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001f44c0
0.11% crowd_perf libc.so.6 [.] 0x00000000000a6403
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000001d11b3
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000d3c36
0.11% crowd_perf libvulkan_radeon.so [.] 0x000000000033df64
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000d3a9c
0.11% crowd_perf libc.so.6 [.] 0x00000000000a6603
0.11% crowd_perf libvulkan_radeon.so [.] 0x00000000000283b6
0.11% crowd_perf libc.so.6 [.] 0x00000000000a456a
0.11% crowd_perf libc.so.6 [.] 0x0000000000189d0b
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000c5581
0.10% crowd_perf libc.so.6 [.] 0x00000000000a5851
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000ce761
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023bdb8
0.10% crowd_perf [unknown] [k] 0xffffffff86355a45
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x000000000023b914
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000022d239
0.10% crowd_perf libc.so.6 [.] 0x0000000000064888
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000bdd81
0.10% crowd_perf libvulkan_radeon.so [.] 0x0000000000231160
0.10% crowd_perf libc.so.6 [.] 0x000000000018a4c0
0.10% crowd_perf libc.so.6 [.] 0x000000000018a56f
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000022d386
0.10% crowd_perf libvulkan_radeon.so [.] 0x00000000000280a5
0.10% crowd_perf libvulkan_radeon.so [.] 0x000000000002cd72
0.10% crowd_perf libSDL3.so.0.4.12 [.] 0x0000000000069865
0.09% crowd_perf libvulkan_radeon.so [.] 0x000000000018a489
0.09% crowd_perf libvulkan_radeon.so [.] 0x000000000033b807
0.09% crowd_perf libvulkan_radeon.so [.] 0x000000000035f6d4
0.09% crowd_perf libvulkan_radeon.so [.] 0x00000000000fa782
0.08% crowd_perf libvulkan_radeon.so [.] 0x00000000000bdfc3
0.03% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000fea2
0.01% crowd_perf libvulkan_radeon.so [.] 0x00000000001d12c7
0.00% wl_cursor_surfa libc.so.6 [.] 0x00000000000a447d
0.00% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x0000000000004951
0.00% crowd_perf libc.so.6 [.] 0x00000000000a5701
0.00% wl_cursor_surfa libwayland-client.so.0.25.0 [.] wl_display_dispatch_queue_pending
0.00% wl_cursor_surfa libwayland-client.so.0.25.0 [.] 0x0000000000003193
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000001dd35f
0.00% crowd_perf libc.so.6 [.] 0x0000000000040242
0.00% wl_cursor_surfa libc.so.6 [.] 0x00000000000a7491
0.00% crowd_perf libc.so.6 [.] 0x00000000000a6ea4
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000027f18
0.00% crowd_perf [unknown] [k] 0xffffffff8632ecd9
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000aed9
0.00% crowd_perf libc.so.6 [.] getenv
0.00% crowd_perf libGLX_nvidia.so.610.43.03 [.] 0x000000000009e60a
0.00% crowd_perf libc.so.6 [.] 0x0000000000094006
0.00% crowd_perf libvulkan_radeon.so [.] 0x00000000001895fc
0.00% crowd_perf libc.so.6 [.] __libc_secure_getenv
0.00% wl_cursor_surfa libc.so.6 [.] 0x00000000000a6ea4
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x00000000000147ed
0.00% crowd_perf [unknown] [k] 0xffffffff850a7e70
0.00% crowd_perf libc.so.6 [.] __ctype_init
0.00% crowd_perf libc.so.6 [.] 0x0000000000064845
0.00% crowd_perf libc.so.6 [.] 0x000000000009741b
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345adb
0.00% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001cd7b4
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345cfe
0.00% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001e3f76
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000028ed0d
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345b18
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345ad6
0.00% crowd_perf libLLVM.so.22.1 [.] std::enable_if<llvm::hashing::detail::is_hashable_data<char const>::value, llvm::hash_code>::type llvm::hashing::detail::hash_combine_range_impl<char const>(char const*, char const*)
0.00% crowd_perf libc.so.6 [.] 0x0000000000097417
0.00% crowd_perf libc.so.6 [.] 0x00000000000a5770
0.00% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001cd7b9
0.00% crowd_perf libc.so.6 [.] 0x0000000000103a63
0.00% crowd_perf libnvidia-glcore.so.610.43.03 [.] 0x0000000000fcd46c
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000018d572
0.00% crowd_perf libdrm.so.2.134.0 [.] drmSyncobjExportSyncFile
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000deb7
0.00% crowd_perf libnvidia-glsi.so.610.43.03 [.] 0x00000000000460e4
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000018d7de
0.00% crowd_perf libdrm.so.2.134.0 [.] drmSyncobjTransfer
0.00% crowd_perf libdrm_amdgpu.so.1.134.0 [.] amdgpu_query_info
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000001fce9
0.00% wl_cursor_surfa libSDL3.so.0.4.12 [.] 0x00000000001e3f78
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000344ddb
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000344cd4
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000344ccb
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000344ddd
0.00% crowd_perf libvulkan_radeon.so [.] 0x0000000000345bb3
0.00% crowd_perf [unknown] [k] 0xffffffff84e01280
0.00% wl_cursor_surfa libc.so.6 [.] 0x0000000000094040
0.00% crowd_perf libc.so.6 [.] 0x000000000011bed0
0.00% crowd_perf libc.so.6 [.] 0x00000000000973a0
0.00% wl_cursor_surfa libc.so.6 [.] 0x0000000000094084
0.00% crowd_perf libc.so.6 [.] 0x0000000000103a40
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000028ece4
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000028ed4d
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000dedf
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000f60f
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000000f611
0.00% crowd_perf ld-linux-x86-64.so.2 [.] 0x000000000001f103
0.00% crowd_perf libLLVM.so.22.1 [.] 0x000000000083a104
0.00% crowd_perf libLLVM.so.22.1 [.] 0x000000000083a146
0.00% crowd_perf libc.so.6 [.] wait4
0.00% crowd_perf libc.so.6 [.] 0x0000000000094040
0.00% crowd_perf libc.so.6 [.] 0x0000000000094047
0.00% crowd_perf libc.so.6 [.] 0x00000000000a58a9
0.00% crowd_perf libdrm.so.2.134.0 [.] drmCommandWrite
0.00% crowd_perf libdrm.so.2.134.0 [.] drmSyncobjTimelineWait
0.00% crowd_perf libnvidia-glcore.so.610.43.03 [.] 0x0000000000fcd46a
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000017fa4b
0.00% crowd_perf libvulkan_radeon.so [.] 0x000000000017fae8
0.00% crowd_perf [unknown] [k] 0xffffffff84e01670
0.00% wl_cursor_surfa libc.so.6 [.] 0x0000000000094050
0.00% wl_cursor_surfa libc.so.6 [.] 0x000000000009408f
0.00% wl_cursor_surfa [unknown] [k] 0xffffffff84e01630
#
# (Tip: For hierarchical output, try: perf report --hierarchy)
#
Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 92 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
Submodule tools/FlameGraph added at 41fee1f99f