Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bd49f61c0 | ||
|
|
dc66203259 | ||
|
|
95723af6a5 | ||
|
|
5e4758ce16 | ||
|
|
7a04cdaa8b | ||
|
|
c49302d90c | ||
|
|
1388c818f4 |
@@ -0,0 +1,208 @@
|
|||||||
|
# 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 33–35% and increased FPS by 49–55%.
|
||||||
|
- 50% reduced frame time by 49–62% and increased FPS by 97–161%.
|
||||||
|
|
||||||
|
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
|
||||||
|
5–6% 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.
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# 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
|
||||||
|
10–20 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.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# 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.
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
.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: shaders-vulkan shaders-d3d12 shaders-metal shaders-all bake toad hello_sprite crowd camera_sandbox clips check test help
|
||||||
.PHONY: flame flame-build flame-record flame-svg flame-report flame-tools
|
.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=crowd_batch
|
# Flamegraph profiling (needs: pacman -S perf). Example: make flame or make flame FLAME_EXAMPLE=toad
|
||||||
FLAME_EXAMPLE ?= crowd
|
FLAME_EXAMPLE ?= crowd
|
||||||
FLAME_BIN := $(FLAME_EXAMPLE)_perf
|
FLAME_BIN := $(FLAME_EXAMPLE)_perf
|
||||||
FLAMEGRAPH_DIR ?= tools/FlameGraph
|
FLAMEGRAPH_DIR ?= tools/FlameGraph
|
||||||
@@ -12,6 +13,19 @@ FLAME_STAMP := $(shell date +%Y%m%d-%H%M%S)
|
|||||||
endif
|
endif
|
||||||
FLAME_PREFIX := $(FLAME_OUT_DIR)/$(FLAME_EXAMPLE)-$(FLAME_STAMP)
|
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:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " bake Bake all characters under content/characters/"
|
@echo " bake Bake all characters under content/characters/"
|
||||||
@@ -24,16 +38,15 @@ help:
|
|||||||
@echo " toad Run the toad example (full demo)"
|
@echo " toad Run the toad example (full demo)"
|
||||||
@echo " hello_sprite Minimal load + draw"
|
@echo " hello_sprite Minimal load + draw"
|
||||||
@echo " crowd Many sprites, one Character_Data"
|
@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 " camera_sandbox Pan camera / Space toggles follow"
|
||||||
@echo " clips Keys 1/2 switch idle/walk"
|
@echo " clips Keys 1/2 switch idle/walk"
|
||||||
@echo " flame Build+record+SVG+JPG+text report (FLAME_EXAMPLE=$(FLAME_EXAMPLE))"
|
@echo " flame Build+record+SVG+JPG+text report (FLAME_EXAMPLE=$(FLAME_EXAMPLE))"
|
||||||
@echo " flame-build Optimized debug binary ($(FLAME_BIN))"
|
@echo " flame-build Debug binary only ($(FLAME_BIN))"
|
||||||
@echo " flame-record perf record (play, then quit)"
|
@echo " flame-record perf record (play, then quit)"
|
||||||
@echo " flame-svg Convert perf.data -> $(FLAME_PREFIX).svg/.jpg"
|
@echo " flame-svg Convert perf.data -> $(FLAME_PREFIX).svg/.jpg"
|
||||||
@echo " flame-report Convert perf.data -> $(FLAME_PREFIX)-report.txt"
|
@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:
|
bake:
|
||||||
./scripts/bake_all.sh
|
./scripts/bake_all.sh
|
||||||
@@ -57,12 +70,31 @@ check:
|
|||||||
odin check examples/toad -collection:pkg=.
|
odin check examples/toad -collection:pkg=.
|
||||||
odin check examples/hello_sprite -collection:pkg=.
|
odin check examples/hello_sprite -collection:pkg=.
|
||||||
odin check examples/crowd -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/camera_sandbox -collection:pkg=.
|
||||||
odin check examples/clips -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:
|
toad:
|
||||||
odin run examples/toad -collection:pkg=.
|
odin run examples/toad -collection:pkg=.
|
||||||
|
|
||||||
@@ -72,15 +104,6 @@ hello_sprite:
|
|||||||
crowd:
|
crowd:
|
||||||
odin run examples/crowd -collection:pkg=.
|
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:
|
camera_sandbox:
|
||||||
odin run examples/camera_sandbox -collection:pkg=.
|
odin run examples/camera_sandbox -collection:pkg=.
|
||||||
|
|
||||||
@@ -94,7 +117,7 @@ flame-tools:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
flame-build:
|
flame-build:
|
||||||
odin build examples/$(FLAME_EXAMPLE) -collection:pkg=. -out:$(FLAME_BIN) -debug -o:speed
|
odin build examples/$(FLAME_EXAMPLE) -collection:pkg=. -out:$(FLAME_BIN) -debug
|
||||||
|
|
||||||
flame-record: flame-build
|
flame-record: flame-build
|
||||||
@echo ">>> Profiling ./$(FLAME_BIN) — play for ~10–20s under load, then quit the window."
|
@echo ">>> Profiling ./$(FLAME_BIN) — play for ~10–20s under load, then quit the window."
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
.agentbox/
|
|
||||||
/agentbox
|
|
||||||
examples/mover/mover
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
.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 ./...
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
# 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 ./...
|
|
||||||
```
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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`)
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
# 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/)
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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"]
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"command": "mover",
|
|
||||||
"window_title": "Agentbox Mover"
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module agentbox
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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, "'", "'\"'\"'") + "'"
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
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 ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// 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
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
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[:]))
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
@@ -118,63 +118,6 @@ set_sprite_clip_switch_and_guards :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, sprite.time, f32(0.09))
|
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)
|
@(test)
|
||||||
spawn_sprite_valid_and_invalid :: proc(t: ^testing.T) {
|
spawn_sprite_valid_and_invalid :: proc(t: ^testing.T) {
|
||||||
data := make_test_character()
|
data := make_test_character()
|
||||||
|
|||||||
+12
-137
@@ -16,15 +16,8 @@ SPRITE_VERTS_SIZE :: SPRITE_VERT_COUNT * size_of(Vertex)
|
|||||||
VERTEX_BUFFER_SIZE :: MAX_SPRITES * SPRITE_VERTS_SIZE
|
VERTEX_BUFFER_SIZE :: MAX_SPRITES * SPRITE_VERTS_SIZE
|
||||||
|
|
||||||
Queued_Sprite :: struct {
|
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,
|
texture: ^sdl.GPUTexture,
|
||||||
|
verts: [SPRITE_VERT_COUNT]Vertex,
|
||||||
}
|
}
|
||||||
|
|
||||||
App :: struct {
|
App :: struct {
|
||||||
@@ -72,7 +65,7 @@ init :: proc(app: ^App, title: cstring, width, height: i32) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requested: sdl.GPUShaderFormat = {.SPIRV, .DXIL, .MSL}
|
requested: sdl.GPUShaderFormat = {.SPIRV, .DXIL, .MSL}
|
||||||
app.device = sdl.CreateGPUDevice(requested, false, nil)
|
app.device = sdl.CreateGPUDevice(requested, true, nil)
|
||||||
if app.device == nil {
|
if app.device == nil {
|
||||||
fmt.eprintfln("CreateGPUDevice failed: %s", sdl.GetError())
|
fmt.eprintfln("CreateGPUDevice failed: %s", sdl.GetError())
|
||||||
return false
|
return false
|
||||||
@@ -83,10 +76,6 @@ init :: proc(app: ^App, title: cstring, width, height: i32) -> bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sdl.SetGPUAllowedFramesInFlight(app.device, 3) {
|
|
||||||
fmt.eprintfln("SetGPUAllowedFramesInFlight failed: %s", sdl.GetError())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer uncapped present for profiling; fall back if unsupported.
|
// Prefer uncapped present for profiling; fall back if unsupported.
|
||||||
present := sdl.GPUPresentMode.VSYNC
|
present := sdl.GPUPresentMode.VSYNC
|
||||||
if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .IMMEDIATE) {
|
if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .IMMEDIATE) {
|
||||||
@@ -253,11 +242,7 @@ end_frame :: proc(app: ^App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
n := len(app.draw_list)
|
n := len(app.draw_list)
|
||||||
batches: [dynamic]Draw_Batch
|
|
||||||
defer delete(batches)
|
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
prepare_draw_batches(app.draw_list[:], &batches)
|
|
||||||
|
|
||||||
map_ptr := sdl.MapGPUTransferBuffer(app.device, app.transfer_buffer, false)
|
map_ptr := sdl.MapGPUTransferBuffer(app.device, app.transfer_buffer, false)
|
||||||
if map_ptr == nil {
|
if map_ptr == nil {
|
||||||
fmt.eprintfln("MapGPUTransferBuffer failed: %s", sdl.GetError())
|
fmt.eprintfln("MapGPUTransferBuffer failed: %s", sdl.GetError())
|
||||||
@@ -301,20 +286,26 @@ end_frame :: proc(app: ^App) {
|
|||||||
app.render_pass = sdl.BeginGPURenderPass(cmd, &color_info, 1, nil)
|
app.render_pass = sdl.BeginGPURenderPass(cmd, &color_info, 1, nil)
|
||||||
sdl.BindGPUGraphicsPipeline(app.render_pass, app.pipeline)
|
sdl.BindGPUGraphicsPipeline(app.render_pass, app.pipeline)
|
||||||
|
|
||||||
for batch in batches {
|
i := 0
|
||||||
|
for i < n {
|
||||||
|
run := texture_run_len(app.draw_list[:], i)
|
||||||
|
q0 := app.draw_list[i]
|
||||||
|
|
||||||
sampler_binding := sdl.GPUTextureSamplerBinding {
|
sampler_binding := sdl.GPUTextureSamplerBinding {
|
||||||
texture = batch.texture,
|
texture = q0.texture,
|
||||||
sampler = app.sampler,
|
sampler = app.sampler,
|
||||||
}
|
}
|
||||||
sdl.BindGPUFragmentSamplers(app.render_pass, 0, &sampler_binding, 1)
|
sdl.BindGPUFragmentSamplers(app.render_pass, 0, &sampler_binding, 1)
|
||||||
|
|
||||||
vb_binding := sdl.GPUBufferBinding {
|
vb_binding := sdl.GPUBufferBinding {
|
||||||
buffer = app.vertex_buffer,
|
buffer = app.vertex_buffer,
|
||||||
offset = u32(batch.start * SPRITE_VERTS_SIZE),
|
offset = u32(i * SPRITE_VERTS_SIZE),
|
||||||
}
|
}
|
||||||
sdl.BindGPUVertexBuffers(app.render_pass, 0, &vb_binding, 1)
|
sdl.BindGPUVertexBuffers(app.render_pass, 0, &vb_binding, 1)
|
||||||
|
|
||||||
sdl.DrawGPUPrimitives(app.render_pass, u32(batch.count * SPRITE_VERT_COUNT), 1, 0, 0)
|
sdl.DrawGPUPrimitives(app.render_pass, u32(run * SPRITE_VERT_COUNT), 1, 0, 0)
|
||||||
|
|
||||||
|
i += run
|
||||||
}
|
}
|
||||||
sdl.EndGPURenderPass(app.render_pass)
|
sdl.EndGPURenderPass(app.render_pass)
|
||||||
app.render_pass = nil
|
app.render_pass = nil
|
||||||
@@ -463,119 +454,3 @@ texture_run_len :: proc(list: []Queued_Sprite, start: int) -> int {
|
|||||||
|
|
||||||
return n
|
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 1–2 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])
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,18 +7,6 @@ fake_tex :: proc(id: uintptr) -> ^sdl.GPUTexture {
|
|||||||
return cast(^sdl.GPUTexture)id
|
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)
|
@(test)
|
||||||
texture_run_len_empty_or_oob :: proc(t: ^testing.T) {
|
texture_run_len_empty_or_oob :: proc(t: ^testing.T) {
|
||||||
testing.expect_value(t, texture_run_len(nil, 0), 0)
|
testing.expect_value(t, texture_run_len(nil, 0), 0)
|
||||||
@@ -74,356 +62,3 @@ 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, 1), 1)
|
||||||
testing.expect_value(t, texture_run_len(list, 2), 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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,15 +27,6 @@ choose_shader_runtime_prefers_msl :: proc(t: ^testing.T) {
|
|||||||
testing.expect_value(t, rt.format, sdl.GPUShaderFormat{.MSL})
|
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)
|
@(test)
|
||||||
choose_shader_runtime_spirv_only :: proc(t: ^testing.T) {
|
choose_shader_runtime_spirv_only :: proc(t: ^testing.T) {
|
||||||
rt, ok := choose_shader_runtime_from_formats({.SPIRV})
|
rt, ok := choose_shader_runtime_from_formats({.SPIRV})
|
||||||
|
|||||||
+28
-71
@@ -5,14 +5,12 @@ import sdl "vendor:sdl3"
|
|||||||
Vec2 :: [2]f32
|
Vec2 :: [2]f32
|
||||||
|
|
||||||
Sprite :: struct {
|
Sprite :: struct {
|
||||||
data: ^Character_Data,
|
data: ^Character_Data,
|
||||||
position: Vec2,
|
position: Vec2,
|
||||||
clip: string,
|
clip: string,
|
||||||
clip_def: Clip_Def,
|
frame: int,
|
||||||
has_clip: bool,
|
time: f32,
|
||||||
frame: int,
|
flip_x: bool,
|
||||||
time: f32,
|
|
||||||
flip_x: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spawn_sprite :: proc(
|
spawn_sprite :: proc(
|
||||||
@@ -26,14 +24,16 @@ spawn_sprite :: proc(
|
|||||||
position = position,
|
position = position,
|
||||||
}
|
}
|
||||||
set_sprite_clip(&sprite, clip)
|
set_sprite_clip(&sprite, clip)
|
||||||
if frame != 0 && sprite.has_clip {
|
if frame != 0 {
|
||||||
c := sprite.clip_def
|
c, ok := character_clip(sprite.data, sprite.clip)
|
||||||
if frame < 0 {
|
if ok {
|
||||||
sprite.frame = 0
|
if frame < 0 {
|
||||||
} else if frame >= len(c.frames) {
|
sprite.frame = 0
|
||||||
sprite.frame = len(c.frames) - 1
|
} else if frame >= len(c.frames) {
|
||||||
} else {
|
sprite.frame = len(c.frames) - 1
|
||||||
sprite.frame = frame
|
} else {
|
||||||
|
sprite.frame = frame
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sprite
|
return sprite
|
||||||
@@ -42,9 +42,9 @@ spawn_sprite :: proc(
|
|||||||
update_sprite :: proc(sprite: ^Sprite, dt: f32) {
|
update_sprite :: proc(sprite: ^Sprite, dt: f32) {
|
||||||
if sprite == nil || sprite.data == nil do return
|
if sprite == nil || sprite.data == nil do return
|
||||||
if dt <= 0 do return
|
if dt <= 0 do return
|
||||||
if !sprite.has_clip do return
|
|
||||||
|
|
||||||
clip := sprite.clip_def
|
clip, ok := character_clip(sprite.data, sprite.clip)
|
||||||
|
if !ok do return
|
||||||
|
|
||||||
frame_count := len(clip.frames)
|
frame_count := len(clip.frames)
|
||||||
if frame_count <= 0 do return
|
if frame_count <= 0 do return
|
||||||
@@ -82,31 +82,7 @@ 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 :: 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 {
|
if app.cmd == nil || app.swapchain_texture == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -116,13 +92,12 @@ draw_sprite_batched :: proc(app: ^App, sprite: ^Sprite, batch_group: u32) {
|
|||||||
if len(app.draw_list) >= MAX_SPRITES {
|
if len(app.draw_list) >= MAX_SPRITES {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !sprite.has_clip do return
|
|
||||||
if sprite.frame < 0 || sprite.frame >= len(sprite.clip_def.frames) {
|
frame, ok := character_frame(sprite.data, sprite.clip, sprite.frame)
|
||||||
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
frame := sprite.clip_def.frames[sprite.frame]
|
|
||||||
|
|
||||||
src_w := f32(frame.source_size[0])
|
src_w := f32(frame.source_size[0])
|
||||||
src_h := f32(frame.source_size[1])
|
src_h := f32(frame.source_size[1])
|
||||||
if src_w <= 0 do src_w = f32(frame.rect[2])
|
if src_w <= 0 do src_w = f32(frame.rect[2])
|
||||||
@@ -149,8 +124,10 @@ draw_sprite_batched :: proc(app: ^App, sprite: ^Sprite, batch_group: u32) {
|
|||||||
|
|
||||||
sw := f32(app.swapchain_w)
|
sw := f32(app.swapchain_w)
|
||||||
sh := f32(app.swapchain_h)
|
sh := f32(app.swapchain_h)
|
||||||
points := sprite_quad_to_clip(x0_px, y0_px, x1_px, y1_px, sw, sh)
|
p0 := to_clip(x0_px, y0_px, sw, sh)
|
||||||
p0, p1, p2, p3 := points[0], points[1], points[2], points[3]
|
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)
|
||||||
|
|
||||||
tex_w := f32(sprite.data.width)
|
tex_w := f32(sprite.data.width)
|
||||||
tex_h := f32(sprite.data.height)
|
tex_h := f32(sprite.data.height)
|
||||||
@@ -165,38 +142,18 @@ draw_sprite_batched :: proc(app: ^App, sprite: ^Sprite, batch_group: u32) {
|
|||||||
{pos = p3, uv = {u0, v1}},
|
{pos = p3, uv = {u0, v1}},
|
||||||
}
|
}
|
||||||
|
|
||||||
append(
|
append(&app.draw_list, Queued_Sprite{texture = sprite.data.texture, verts = verts})
|
||||||
&app.draw_list,
|
|
||||||
Queued_Sprite {
|
|
||||||
texture = sprite.data.texture,
|
|
||||||
verts = verts,
|
|
||||||
batch_group = batch_group,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set_sprite_clip :: proc(sprite: ^Sprite, clip: string) {
|
set_sprite_clip :: proc(sprite: ^Sprite, clip: string) {
|
||||||
if sprite == nil || sprite.data == nil do return
|
if sprite == nil || sprite.data == nil do return
|
||||||
|
|
||||||
if sprite.clip == clip && sprite.has_clip do return
|
if sprite.clip == clip do return
|
||||||
|
|
||||||
def, ok := character_clip(sprite.data, clip)
|
_, ok := character_clip(sprite.data, clip)
|
||||||
if !ok do return
|
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 = clip
|
||||||
sprite.clip_def = def
|
|
||||||
sprite.has_clip = true
|
|
||||||
sprite.frame = 0
|
sprite.frame = 0
|
||||||
sprite.time = 0
|
sprite.time = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
|
||||||
import eng "pkg:engine"
|
import eng "pkg:engine"
|
||||||
|
|
||||||
// Many sprites sharing one Character_Data (Flyweight) — good batching demo.
|
// Many sprites sharing one Character_Data (Flyweight) — good batching demo.
|
||||||
COUNT :: eng.MAX_SPRITES
|
COUNT :: 24
|
||||||
COLS :: 16
|
|
||||||
FRAME_LOG_EVERY :: 60
|
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
app: eng.App
|
app: eng.App
|
||||||
@@ -19,45 +16,25 @@ main :: proc() {
|
|||||||
|
|
||||||
sprites: [COUNT]eng.Sprite
|
sprites: [COUNT]eng.Sprite
|
||||||
for i in 0 ..< COUNT {
|
for i in 0 ..< COUNT {
|
||||||
col := i % COLS
|
col := i % 8
|
||||||
row := i / COLS
|
row := i / 8
|
||||||
pos := eng.Vec2 {
|
pos := eng.Vec2 {
|
||||||
f32(40 + col * 48),
|
f32(120 + col * 80),
|
||||||
f32(80 + row * 60),
|
f32(280 + row * 120),
|
||||||
}
|
}
|
||||||
clip := "idle" if (i % 2) == 0 else "walk"
|
clip := "idle" if (i % 2) == 0 else "walk"
|
||||||
sprites[i] = eng.spawn_sprite(&data, pos, clip, i % 5)
|
sprites[i] = eng.spawn_sprite(&data, pos, clip, i % 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look at the middle of the grid
|
||||||
app.camera.position = {400, 400}
|
app.camera.position = {400, 400}
|
||||||
|
|
||||||
last := eng.now_seconds()
|
last := eng.now_seconds()
|
||||||
frame_i := 0
|
|
||||||
sum_ms: f64
|
|
||||||
peak_ms: f64
|
|
||||||
|
|
||||||
for eng.events() {
|
for eng.events() {
|
||||||
now := eng.now_seconds()
|
now := eng.now_seconds()
|
||||||
dt := f32(now - last)
|
dt := f32(now - last)
|
||||||
last = now
|
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 {
|
for &s in sprites {
|
||||||
eng.update_sprite(&s, dt)
|
eng.update_sprite(&s, dt)
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,783 +0,0 @@
|
|||||||
# 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.
|
Before Width: | Height: | Size: 124 KiB |
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 82 KiB |
@@ -1,730 +0,0 @@
|
|||||||
# 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.
|
Before Width: | Height: | Size: 144 KiB |
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 92 KiB |
Binary file not shown.
Submodule tools/FlameGraph deleted from 41fee1f99f
Reference in New Issue
Block a user