Compare commits

...

660 Commits

Author SHA1 Message Date
Bartosz Taudul
30997d5ca6 Release 0.14.1. 2026-08-22 19:06:43 +02:00
Bartosz Taudul
aa53e377e7 Use xdg-foreign v2 protocol for Wayland file dialog parent window. 2026-08-22 18:31:26 +02:00
Bartosz Taudul
cae9bf0bef Decrease rate of event production in test application. 2026-08-22 14:31:58 +02:00
Bartosz Taudul
4d3a84a97e Refine GUI verify skill name and description. 2026-08-22 13:07:05 +02:00
Bartosz Taudul
10926caed4 Add tooltip stating statistics limit also affects source view. 2026-08-22 12:57:34 +02:00
Bartosz Taudul
073152a10a Support time range limit entries tooltips. 2026-08-22 12:57:34 +02:00
Bartosz Taudul
2ec2c2c8ac Mention that statistics range limit also affects source view. 2026-08-22 12:41:29 +02:00
Bartosz Taudul
dce2c710e5 Merge pull request #1459 from alandtse/feat/mcp-sdk-v2
feat(mcp): migrate to MCP Python SDK v2
2026-08-22 11:04:22 +02:00
Alan Tse
e670bd18f7 feat(mcp): migrate to MCP Python SDK v2
`pip install mcp` now installs 2.x by default (v1 is
maintenance-only going forward), and nothing in this repo pinned a
version, so a fresh install already broke: v2 removed
mcp.server.fastmcp entirely, no compat shim.

- Import/class: mcp.server.fastmcp.FastMCP ->
  mcp.server.mcpserver.MCPServer.
- Transport options (host, port, sse_path, streamable_http_path)
  moved off the constructor and the settings object onto run();
  mutating mcp_server.settings.port now raises ValueError.
- _http_ping and the startup message read _SSE_PATH/
  _STREAMABLE_HTTP_PATH constants instead of settings.sse_path/
  settings.streamable_http_path -- v2 no longer exposes them to
  read back. Values match the SDK's own defaults in both versions
  ("/sse", "/mcp"), so served paths are unchanged.
- Adds extra/mcp/requirements.txt (mcp>=2.0.0,<3) -- there was no
  dependency manifest at all before this.

Decorator API is unchanged; every tool/resource here was already
async def, so the "sync handlers now run on worker threads" change
doesn't apply.

Verified in an isolated venv (mcp 2.0.0): module imports cleanly
with all 10 tools and 2 resources registered, the server starts
and serves streamable-http on the expected path, and _http_ping
correctly reports a running instance as alive.
2026-08-21 20:42:28 -07:00
Bartosz Taudul
1dfe828cdf Do not assert on zero content length in HttpRequest. 2026-08-22 02:26:54 +02:00
Bartosz Taudul
23ffe89d24 Abort pending update check on profiler exit. 2026-08-22 02:20:24 +02:00
Bartosz Taudul
f5938e0336 Remove unused Socket::ConnectBlocking. 2026-08-22 02:20:24 +02:00
Bartosz Taudul
86ea59bbe3 Make HttpRequest interruptible. 2026-08-22 02:20:24 +02:00
Bartosz Taudul
56f5d1239d Add IsConnecting and interruptible ReadUpTo to Socket. 2026-08-22 02:20:21 +02:00
Bartosz Taudul
cc04578f68 Derive meson and python package versions from TracyVersion.hpp. 2026-08-21 23:05:58 +02:00
Bartosz Taudul
39722e1e3f Skip timeline drawing when no width is available. 2026-08-21 22:10:07 +02:00
Bartosz Taudul
78a7553c6f Guarantee forward progress in the plot preprocess loop. 2026-08-21 22:10:05 +02:00
Bartosz Taudul
cdd5940b44 Define TRACY_NO_FILESELECTOR when the NO_FILESELECTOR option is set.
The CMake option only controlled whether NFD is built and linked, but
the file selector code is guarded by the TRACY_NO_FILESELECTOR macro,
which the build never defined - configuring with -DNO_FILESELECTOR=ON
failed to compile (nfd.h not found in TracyFileselector.cpp,
BackendWayland.cpp and BackendGlfw.cpp). Define the macro for the
profiler target when the option is set.
2026-08-21 20:20:45 +02:00
Bartosz Taudul
246139fb04 Show the NFD error message in the file selector failure popup.
When the native dialog failed, the popup said "File selector cannot
be displayed. Check nfd library implementation for details." but never
showed any details: NFD_GetError() was never read, so the actual
failure reason (e.g. the xdg-desktop-portal lacking a FileChooser
implementation on the session) was invisible and the user was stuck
without a way to open or save traces.

Capture NFD_GetError() into a static string when
NFD_OpenDialogU8_With()/NFD_SaveDialogU8_With() return NFD_ERROR and
display it in the popup. The copy is required: the returned pointer
references NFD-internal storage (the D-Bus error message or a static
buffer) that is overwritten by the next NFD call or cleared on
shutdown. In TRACY_NO_FILESELECTOR builds no NFD exists, so the
previous fallback line is kept there.

The error is shown as a single line (TextUnformatted) rather than
wrapped: in an AlwaysAutoResize window the wrap width is derived from
the previous frame's window size, which feeds back into the auto-size
and oscillates, converging to a narrow multi-line column for a ~110
character D-Bus message.
2026-08-21 20:20:45 +02:00
Bartosz Taudul
5257132d97 Indent grouped child zone rows in the zone info window.
When a child group was expanded, the member rows were not visibly
indented under the group name: each row starts at the tree indent
(column 0 is IndentEnable by default, and TableBeginRow() latches the
open group's TreePush indent), so a single Indent() put the label two
IndentSpacing from the cell origin. The group header, however, leads
with the source location color box plus item spacing before its tree
arrow, so its name starts four IndentSpacing further right than the
member labels - the child list read as a continuation of the header
row instead of a subtree.

Add a second Indent() (and the balancing Unindent()) so the member
labels land clearly to the right of the group name. The full-row
Selectable behavior is unchanged. GPU zones are unaffected: their
rows carry no color box, so the member labels were already one level
right of the group name.
2026-08-21 20:20:44 +02:00
Bartosz Taudul
db851737e0 Prevent ini settings from reversing sortable table column order.
The imgui 1.92.9b ini writer omits 'Column N' lines for columns without
saved data (e.g. the unsized, unsorted 'Zone' column of the child zones
table, which only persists the default Time sort). On restore, the
missing entry keeps Index == -1, the sequential fallback in
TableLoadSettingsForColumns() assigns it to the remaining column, and
TableFixDisplayOrder() sorts DisplayOrder -1 to the end - flipping the
column order (Time | Zone in the zone info child list) for the session.
Guard the non-reorderable load path against phantom entries.

Upstream: https://github.com/ocornut/imgui/issues/9519
2026-08-21 20:20:42 +02:00
Bartosz Taudul
73917aaef5 Sort single-member child groups by zone name in the zone info window.
The group name-sort key was always the source location name, but
single-member groups display the zone-level name, which prefers the
runtime ZoneName() string. Such groups sorted by a name the user could
not see. The key now mirrors the display: zone name for single-member
groups, source location name for the rest. GPU zones are unaffected
(their names always resolve from the source location).
2026-08-21 02:03:50 +02:00
Bartosz Taudul
f0fce23bb6 Allow sorting child zones by name in the zone info window.
The child zone list was always sorted by time. It is now a sortable
table with Zone and Time columns, like the statistics view and the
time distribution table in the same window: Time is the default sort
(descending, as before), Zone sorts by name with time as the
tiebreak, and groups plus their members follow the same selection.
The sort choice persists across zones and the group toggle. Group
and child names are looked up once per draw only when name-sorting.
2026-08-21 02:03:50 +02:00
Bartosz Taudul
ad7a1744fa Add web gui test skill. 2026-08-21 02:03:47 +02:00
Bartosz Taudul
62bffd085c Merge pull request #1458 from TravisGesslein/master
D3D12: pass an empty written range when unmapping the readback buffer
2026-08-20 20:46:16 +02:00
Travis Gesslein
f8a75703f9 D3D12: pass an empty written range when unmapping the readback buffer
Otherwise on unmap you can get the validation layer error:

"ID3D12Resource3::ID3D12Resource::Unmap: pWrittenRange does not point to an empty D3D12_RANGE and
the heap type is D3D12_HEAP_TYPE_READBACK. Readback resources can be written by the CPU but there's
not much utility. The rationale is that readback heaps are stuck in COPY_DEST state such that the
GPU can never use what the CPU is writing. The range [0, 524288) should be empty (Begin >= End)."

Check also https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12resource-unmap

"This indicates the region the CPU might have modified" -> tracy essentially declares the entire range as written, but it's not even a cpu->gpu write buffer -> bug.
2026-08-20 17:36:39 +02:00
Bartosz Taudul
e3cde055cd Export Tracy version in the installed CMake package.
TracyConfig.cmake now sets TRACY_VERSION_STRING and a
TracyConfigVersion.cmake is generated and installed, so
find_package(Tracy X.Y CONFIG) works against installed trees.
Compatibility mode is SameMinorVersion: for a 0.x project the minor
version is the compatibility unit, and consumers who need to pin the
exact version can use find_package(... EXACT). SamePatchVersion is
not an option - it requires CMake 4.4 while the project minimum is
3.13.
2026-08-20 01:03:49 +02:00
Bartosz Taudul
0e37b94e6c Add TracyAssert.hpp to CMake / Meson. 2026-08-20 01:03:47 +02:00
Bartosz Taudul
6b58f7d8d0 Report system tracing setup failures.
SysTraceStart previously reported success even when no perf events
could be opened, leaving a worker thread running over no buffers
and an empty CPU section without explanation; it now fails in that
case. Also log when /proc/kallsyms cannot be read, which otherwise
silently results in ??? kernel stack frames.
2026-08-19 22:18:59 +02:00
Bartosz Taudul
05f0508684 Fix system tracing on kernels older than 4.1.
perf_event_attr.use_clockid only exists since Linux 4.1 and
sample_max_stack since Linux 4.8; older kernels (e.g. the 3.18
kernels of 32-bit Android devices) reject the attributes with
EINVAL/E2BIG, which silently disabled all of system tracing.
Classify the running kernel from uname(2) and only send the fields
its perf_event_open ABI supports.
2026-08-19 22:18:57 +02:00
Bartosz Taudul
e2c05210e7 Add a note about zones crossing multiple on-demand connections. 2026-08-19 20:30:42 +02:00
Bartosz Taudul
275fef576a Bring Lua on-demand connection matching logic up-to-date with C++ side. 2026-08-19 20:30:40 +02:00
Bartosz Taudul
2a3792aaa3 Handle source code queries in no-callstack builds.
The symbol worker thread, which answers source code queries, only exists
when call stack support is compiled in. Handle the query directly in the
main thread so it is always answered and the server can terminate.
2026-08-19 17:54:44 +02:00
Bartosz Taudul
ad99139f8e Include TracyWebGPU.hpp in CMakeLists.txt and meson.build. 2026-08-18 23:20:01 +02:00
Bartosz Taudul
fee9285d68 Use the compiler builtin for the ARM thread id.
The mrc to c13/c0/3 (thread id register) is undefined on ARMv5 (e.g.
ARM926EJ-S) and SIGILL'd during init. __builtin_thread_pointer lowers
to the same mrc on targets with a thread pointer register and to
__aeabi_read_tp on soft-TLS targets.
2026-08-18 22:35:44 +02:00
Bartosz Taudul
a4b51631ed Regenerate markdown manual. 2026-08-18 18:41:32 +02:00
Bartosz Taudul
433ac3629f Document that manual lifetime requires profiler shutdown.
https://github.com/wolfpld/tracy/issues/483
2026-08-18 18:39:45 +02:00
Bartosz Taudul
16d9c15663 Include TracyMangle.hpp in TracyDebug.hpp for consistent delayed init state. 2026-08-18 00:30:28 +02:00
Bartosz Taudul
4e9121eb2f Typos. 2026-08-18 00:28:32 +02:00
Bartosz Taudul
7f7bd69195 Remove TRACY_DELAYED_INIT option.
Delayed init was introduced 2019-02-19 in ef5e30056 and was lazy init at
that time, matching the name.

This changed 2020-05-19 in 4eb78f5c8, when auto-init was added, making
delayed init not delayed anymore.

While the option is still needed to make manual lifetime work, and it has
to be enabled on apple, there's no reason anymore to expose it to users.
2026-08-18 00:26:24 +02:00
Bartosz Taudul
2a7542ccd8 Mangle the GetToken accessor based on config.
The macro fast paths only reference GetProfiler from -O0 code, direct
calls, and ON_DEMAND builds: the single GetProfiler() call on the macro
path sits behind the constexpr-dead callstack guard and is folded away
at -O1+, so Release builds carried no config fingerprint and mismatched
clients linked silently.

GetToken is the hot-path binding of every queueing macro and is
referenced at all optimization levels; mangle it the same way so
mismatches fail at link time in Release builds too.
2026-08-17 22:21:57 +02:00
Bartosz Taudul
328f50211f Link TracyImGui library with libdl.
ImGui OpenGL loader uses dlopen / dlsym / dlclose to load the OpenGL
implementation.

Linking with libdl is not needed with glibc >= 2.34 (released in 2021):

NEWS for version 2.34
=====================

Major new features:

* In order to support smoother in-place-upgrades and to simplify
  the implementation of the runtime all functionality formerly
  implemented in the libraries libpthread, libdl, libutil, libanl has
  been integrated into libc.  New applications do not need to link with
  -lpthread, -ldl, -lutil, -lanl anymore.

(...)

Older glibc versions and also possibly other libc implementations still
need the libdl link.
2026-08-17 22:13:59 +02:00
Bartosz Taudul
f5c53af367 Fix plot color R/B channel swap. 2026-08-17 00:17:19 +02:00
Bartosz Taudul
b751c1c8d0 Fix csvexport --sep and --filter options. 2026-08-16 23:41:09 +02:00
Bartosz Taudul
b6cfdd2d2e Update embed.tracy to 0.14.
Previous was 0.10, which was too old after bumping minver to 0.11.
2026-08-16 15:26:10 +02:00
Bartosz Taudul
453d649c1a Add TRACY_ASSERT for custom assert implementations. 2026-08-16 13:32:44 +02:00
Bartosz Taudul
254f0ad210 Drop support for pre 0.11 traces. 2026-08-16 00:27:30 +02:00
Bartosz Taudul
3f9714059a Add protocol history for 0.14. 2026-08-16 00:04:13 +02:00
Bartosz Taudul
8fdfd08af0 Refactor GPU context id handling.
The gpu backends each used a bare 255 as the sentinel for a
not-yet-initialized context id. Define InvalidGpuContextId (-1) in
TracyQueue.hpp and replace the scattered 255s and their asserts
with it.

Id exhaustion is handled by an error message and an assert in the
new NextGpuContextId() getter. Non-assert builds continue execution,
at which point they are no longer valid. Handling this code path
is out of scope here. Various attempts at handling the exhaustion
problem have been otherwise purged from the API implementations.
2026-08-15 21:46:29 +02:00
Bartosz Taudul
54ecee1f74 Fix memory events not being attributed to fibers.
Alloc and free events stored the OS thread, so the memory view's
Zone alloc/Zone free columns resolved the zone on the OS thread's
timeline instead of the currently-running fiber. Attribute them to
the active fiber the same way zone and message events are
(follow ThreadData::fiber).
2026-08-15 15:15:54 +02:00
Bartosz Taudul
b10d466a38 Regenerate markdown manual. 2026-08-15 13:41:23 +02:00
Bartosz Taudul
86fc7f02cb Document the need to reflect order of allocations in reported events. 2026-08-15 13:21:11 +02:00
Bartosz Taudul
16805b47ef Update TRACY_IGNORE_MEMORY_FAULTS description in CMake/Meson options. 2026-08-15 13:08:44 +02:00
Bartosz Taudul
756411d802 Allow disabling wait stacks capture. 2026-08-14 01:28:22 +02:00
Bartosz Taudul
d579ed3cce Fix VSync on 32-bit win32 targets.
The struct sizes are kernel sizes. A 64-bit kernel is required for
tracing anyway.
2026-08-13 18:34:58 +02:00
Bartosz Taudul
edc09a4e97 Fix 16-bit string length desync on offset recovery
The decoder recovered offset-encoded 16-bit string lengths into a
uint16_t, so sz += ProtocolOffset8Bit truncated lengths in [65536,
65791] back into [0, 255] in release builds, desynchronizing the
stream. Read the wire value into uint16_t sz16 and recover into a
uint32_t sz, relying on automatic promotion for the addition.

Align the client asserts with the encoder's actual capacity
(ProtocolOffset8Bit + uint16 max).
2026-08-13 18:18:58 +02:00
Bartosz Taudul
821d43c6b5 Send string for SectionSetup during on-demand deferred messages replay. 2026-08-13 18:15:20 +02:00
Bartosz Taudul
16329e0a0c Silence MSVC warning C4366.
warning C4366: The result of the unary '&' operator may be unaligned
2026-08-10 18:16:39 +02:00
Bartosz Taudul
099df3de3d Release 0.14.0. 2026-08-09 21:06:31 +02:00
Bartosz Taudul
a7926283a8 Wait for capture workers to disconnect before saving. 2026-08-09 18:32:17 +02:00
Bartosz Taudul
6ce8ad7aff CI: centralize cmake --parallel flag for linux workflow
Define CMAKE_PARALLEL once per environment (--parallel 2 on GitHub CI,
unlimited locally) and use it across every cmake build, so all builds
are OOM-protected, not just the profiler GUI.
2026-08-09 12:46:43 +02:00
Bartosz Taudul
37a7c2c197 Add permissions and concurrency groups to all workflows
Set contents: read permissions on every workflow and add per-workflow
concurrency groups keyed on the git ref to deduplicate concurrent runs.
Release workflow keeps cancel-in-progress: false so a release build is
never canceled; job-level contents: write on attach-to-release is
preserved. Concurrency groups use hardcoded prefixes so reusable
workflows called from release.yml do not inherit the caller workflow
name and collide.
2026-08-09 12:46:40 +02:00
Bartosz Taudul
a8f9faf496 Fix parallel flags for profiler GUI build in linux workflow.
GitHub runners: limit to 2 jobs
Non-gimped environments: use all you got.
2026-08-09 12:11:19 +02:00
Bartosz Taudul
f57986e17d Add parallel flag to library build in linux workflow. 2026-08-09 12:11:04 +02:00
Bartosz Taudul
5f647e7eaf Build dyna example multithreaded in linux workflow. 2026-08-09 12:02:20 +02:00
Bartosz Taudul
e06fc5211b No meson/ninja needed in windows workflow. 2026-08-09 11:56:48 +02:00
Bartosz Taudul
01a9e6d3af No version for meson. 2026-08-08 02:15:18 +02:00
Bartosz Taudul
c228c662bb Install TracyVersion.hpp in CMake and meson
Ship the version header alongside the rest of public/common so installed
users can query the Tracy version at compile time; it is the single
source of truth CMake already parses via cmake/version.cmake.
2026-08-08 02:09:08 +02:00
Bartosz Taudul
30d14caab6 Install TracyFormat.h in CMake and meson
TracyFormat.h is transitively included by installed public headers
(TracyC.h, TracyProfiler.hpp, TracyScoped.hpp) but was missing from
the common_includes install list in both build systems, breaking
installed users. Add it to the list in CMakeLists.txt and meson.build.
2026-08-08 02:09:08 +02:00
Bartosz Taudul
5d2c353335 Fix TRACY_NO_CODE_TRANSFER source code transfer. 2026-08-07 20:44:51 +02:00
Bartosz Taudul
92f0e0e951 Make linux-cli workflow compatible with act runner. 2026-08-07 00:28:10 +02:00
Bartosz Taudul
7dafaca602 Stage linux-cli artifact in a relative path for container job 2026-08-07 00:25:13 +02:00
Bartosz Taudul
18eaa221d3 Regenerate markdown manual. 2026-08-06 22:37:24 +02:00
Bartosz Taudul
6bca1141fe Update manual. 2026-08-06 22:37:02 +02:00
Bartosz Taudul
a5d155250b Update NEWS. 2026-08-06 22:37:02 +02:00
Bartosz Taudul
63c94bae7d Strip release binaries on Linux and macOS. 2026-08-06 22:36:59 +02:00
Bartosz Taudul
8163734842 Ship Linux CLI tools in a versioned release zip
Build the CLI tools in a dedicated ubuntu:24.04 workflow and pack them
with the AppImage into a single linux-<version>.zip, matching the
Windows/macOS release shape.
2026-08-06 22:22:09 +02:00
Bartosz Taudul
c119f1c8de Package tracy-capture-daemon in release binaries. 2026-08-06 21:22:02 +02:00
Bartosz Taudul
dcc86d343b Link to cropped guadec video. 2026-08-06 21:21:26 +02:00
Bartosz Taudul
507c77ba41 Move continuation words wider than the leftover whole to the next line.
ImGui's CalcWordWrapPosition() only cuts a word mid-word when it fits on
no line, i.e. when it is wider than the full line width. PrintTextWrapped
passes the leftover width for a glued continuation segment's first line,
so a continuation word wider than the leftover (but fitting on the next
full-width line) was being cut mid-word. Detect that case and move the
whole word to the next line instead.
2026-08-05 01:37:05 +02:00
Bartosz Taudul
eacb64a714 Hack around lack of flag to always enable scrollbar in a table.
https://github.com/ocornut/imgui/issues/9499#issuecomment-5182563922
2026-08-04 19:43:55 +02:00
Bartosz Taudul
bdb0f3c3d7 Replace vertical / horizontal AddLine calls with AddLineV / AddLineH. 2026-08-04 19:00:59 +02:00
Bartosz Taudul
757c2fff30 Grammar. 2026-08-03 22:23:19 +02:00
Bartosz Taudul
a6d96d4201 Ignore all-newline message chunks at the beginning of LLM content.
DeepSeek 4 Flash likes to send "\n\n" as content between the reasoning
and tool call sections.
2026-08-03 22:23:03 +02:00
Bartosz Taudul
1c2a8b4c40 Filter out thinking, tool calls, attachments from the summary and suggestion. 2026-08-03 22:23:03 +02:00
Bartosz Taudul
6dd09e3ed8 Make the tool reply limit easier to understand. 2026-08-03 20:09:24 +02:00
Bartosz Taudul
96dcd7a38b Source tooltip may render more than one active line. 2026-08-03 18:26:49 +02:00
Bartosz Taudul
0290f0ba54 Give instructions for providing source file ranges. 2026-08-03 18:26:49 +02:00
Bartosz Taudul
1866cf25b9 Support extracting lines range. 2026-08-03 18:26:49 +02:00
Bartosz Taudul
d91769b72e Check if there's a parent directory to go to on .. in NormalizePath. 2026-08-03 18:26:49 +02:00
Bartosz Taudul
d0ffd8dc86 Merge pull request #1448 from 17steen/fix/cuda-defer-gpu-new-context
Defer CUDA GpuNewContext for on-demand connections
2026-08-03 12:31:21 +02:00
Robin Oger
5df9ac73c7 Defer CUDA GpuNewContext for on-demand connections.
CUDACtx's constructor writes GpuNewContext directly through
QueueSerialFinish(), unlike every other GPU backend (Vulkan, OpenGL,
D3D11/12, Metal, WebGPU, Rocprof), which all defer it via
GetProfiler().DeferItem() so it survives on-demand's per-connection
queue clear.

A profiler connecting any time after the CUDA context is created (in
practice: any time after process start) never receives GpuNewContext.
The GpuContextName message that Name() sends right after (already
correctly deferred) then crashes the server's
Worker::ProcessGpuContextName with an unregistered context id
(assert(ctx) fails; undefined behavior in release builds).

Same fix already applied to the Rocprof backend in #1336. Fixes #1171.

Includes a repro test under tests/cuda/repro/on_demand/, mirroring the
structure #1336 added for Rocprof: a minimal CUDA program that creates
an on-demand context (repro.cu/CMakeLists.txt), and a check_gpu_zones
tool that loads the resulting .tracy file and verifies the GPU context
was named and populated with zones. Verified locally: unpatched
tracy-capture crashes on the first connection attempt; patched, three
consecutive connect/disconnect cycles all succeed and check_gpu_zones
reports a named context with recorded zones.
2026-08-03 10:48:20 +02:00
Bartosz Taudul
facbd3c0a7 Add connectionId protection to GPU zone scopes. 2026-08-02 20:44:48 +02:00
Bartosz Taudul
df29943d78 Add TRACY_ON_DEMAND guards to C API GPU functions. 2026-08-02 20:44:46 +02:00
Bartosz Taudul
9209bcdafd Add connectionId protection to C API zone functions. 2026-08-02 19:57:13 +02:00
Bartosz Taudul
8c8d451a2d Merge pull request #1446 from nyanpasu64/fork
Add formatted zone text/name macros to C API
2026-08-02 11:51:53 +02:00
nyanpasu64
bfdf647114 Rename TracyFormat.hpp to .h since it's used in the C API 2026-08-01 18:19:01 -07:00
nyanpasu64
0558301e49 Address LLM review comments 2026-08-01 18:15:27 -07:00
nyanpasu64
71098f707c TracyCZoneTextF/TracyCZoneNameF 2026-08-01 16:27:55 -07:00
Bartosz Taudul
a1fb20e620 Initialize uninitialized View members 2026-08-01 13:51:47 +02:00
Bartosz Taudul
9e163ad6b9 Remove dead vertical timeline centering toggle 2026-08-01 13:42:51 +02:00
Bartosz Taudul
93a8f86850 Use the mouse event abstraction in timeline click handlers 2026-08-01 13:27:43 +02:00
Bartosz Taudul
5698b45820 Hold worker data lock for the whole draw frame 2026-08-01 12:24:17 +02:00
Bartosz Taudul
95811a658a Save drawFrameTargets and plotHeight as configurable defaults 2026-08-01 11:52:00 +02:00
Bartosz Taudul
37468811c7 Declare ___tracy_get_time with explicit void parameter list. 2026-08-01 11:37:36 +02:00
Bartosz Taudul
a6352890f4 Check buffer bounds before accessing the buffer. 2026-08-01 02:17:59 +02:00
Bartosz Taudul
07b7219498 Use proper buffer size for program name normalization. 2026-08-01 02:15:29 +02:00
Bartosz Taudul
370a41225e The suggested llm query should be from the user's perspective. 2026-08-01 01:42:59 +02:00
Bartosz Taudul
1ad64f19a7 Use ctx.scale in timeline items instead of GetScale(). 2026-08-01 01:38:18 +02:00
Bartosz Taudul
93af7f987a Fix data race on pending thread hints. 2026-08-01 01:38:16 +02:00
Bartosz Taudul
144fec96d3 Do not scan source length over the destination size. 2026-08-01 00:59:29 +02:00
Bartosz Taudul
3448832b72 Replace impactful cases of strcpy with strzcpy. 2026-08-01 00:37:35 +02:00
Bartosz Taudul
c136232c97 Use strzcpy in client library. 2026-08-01 00:06:39 +02:00
Bartosz Taudul
7ee384630b Do not use strncpy in trace info window. 2026-08-01 00:06:39 +02:00
Bartosz Taudul
5ef415619d Add safe string copy. 2026-08-01 00:04:02 +02:00
Bartosz Taudul
8985887128 Don't include GUI profiler deps for all other tools. 2026-07-31 22:01:52 +02:00
Bartosz Taudul
bf21d8ea11 Bump ImGui to 1.92.9b-docking. 2026-07-31 18:12:14 +02:00
Bartosz Taudul
bc85c4faa5 Fix include guard. 2026-07-31 18:12:13 +02:00
Bartosz Taudul
8fd29b1038 Merge pull request #1441 from alandtse/fix/mcp-live-memory-limit
fix(mcp): cap live sessions to avoid OOM
2026-07-31 13:03:45 +02:00
Bartosz Taudul
d46fe6fd9f Merge pull request #1432 from alandtse/feat/mcp-crash-diagnostics
fix(mcp): recover from hangs and evict idle files
2026-07-31 13:03:12 +02:00
Alan Tse
613524247f docs(mcp): document file-idle eviction TTL
The guide claimed "two backstops" and only described the LRU cap
and the disconnected-live TTL, omitting the new file-idle TTL
entirely -- a cold agent reading it would have no idea idle
file-loaded captures now expire too, or what env var controls it.
2026-07-30 22:07:54 -07:00
Alan Tse
a418ec6a03 fix(mcp): evict idle file-loaded captures too
Automatic eviction only ever covered disconnected live instances
(_evict_disconnected_idle explicitly skipped anything with a
path), so a file-loaded capture -- potentially many GB -- stayed
resident forever until the TRACY_MCP_MAX_INSTANCES cap forced an
LRU eviction to make room. list_instances already documented
"unload_capture instead of waiting for automatic eviction" as the
alternative, but no automatic path actually existed for this case.

Generalizes the sweep into _evict_idle: file-loaded instances now
get their own idle-since-last-use TTL (TRACY_MCP_FILE_IDLE_TTL_S,
default 1800s matching the disconnected-live TTL). Safe to evict
on a timer since they're already durably on disk -- load_capture
brings them back. Connected live instances are untouched, same as
before.
2026-07-30 22:07:33 -07:00
Alan Tse
00c2245c49 fix(mcp): recover cleanly from a hung server
Addresses the "server just vanishes" pattern from
alandtse/tracy#2, where Unable to connect gives no way to tell a
segfault apart from a hang and required manually killing an
unresponsive process before restart.

- faulthandler.enable() at startup writes a thread-state
  traceback to tracy_mcp.crash.log on a genuine fatal crash
  (works on Windows via SetUnhandledExceptionFilter).
- The periodic sweep loop now also logs a heartbeat (uptime,
  instance/task counts, evictions) so a hung event loop is
  distinguishable from a dead process by the last timestamp on
  disk.
- _is_our_server_running() now backs its os.kill(pid, 0) check
  with an HTTP self-ping. A deadlocked-but-alive process passed
  the old PID-only check, silently blocking restart; a
  non-responsive server is now reported by PID and a fresh
  instance starts on a new port instead.
2026-07-30 22:06:57 -07:00
Alan Tse
e4fab6af3a fix(mcp): cap live sessions to avoid OOM
live_connect wrapped Worker(addr, port) with no memoryLimit, so
the binding default (-1, unlimited) applied. A long-lived live
session on a busy target grows unbounded -- every zone/message/
memory event stays resident until disconnect -- and can OOM-kill
the whole server process rather than just that one instance.

Worker already has a graceful cap: TracyWorker.cpp's receive loop
checks memoryLimit and calls QueryTerminate() + a clean disconnect
once exceeded, instead of continuing to grow. Just wasn't wired up
from Python.

Adds memory_limit_mb to live_connect (defaults to
TRACY_MCP_LIVE_MEMORY_LIMIT_MB, 8192 if unset; 0 disables),
converts to bytes for the Worker constructor, and reports the
active limit in the connect response.
2026-07-30 21:48:06 -07:00
Bartosz Taudul
b418abd8de Merge pull request #1439 from ivanstepanovftw/fix-dwarf-npd
fix #1438 null dereference in dwarf
2026-07-30 19:25:41 +02:00
Ivan Stepanov
1673a305cf fix #1438 null dereference in dwarf 2026-07-30 14:33:49 +02:00
Bartosz Taudul
3baa2f0a09 Fix typo. 2026-07-29 18:57:03 +02:00
Bartosz Taudul
46b21c0c75 Update ARM CPU identifiers. 2026-07-29 18:54:10 +02:00
Bartosz Taudul
a02513c1a2 Update Tim Apple devices list. 2026-07-29 15:18:46 +02:00
Bartosz Taudul
ead931ef3c Safeguard against zero count entries in statistics list.
There is a legitimate, if rare, way to get nonReentrantCount == 0 with
total != 0 — an outer zone that never terminated before the capture ended,
whose reentrant inner zone did terminate. The outer never contributes to
slz (no end event), but it did increment the stack count, so the inner one
is correctly recorded as a reentry.
2026-07-28 23:49:01 +02:00
Bartosz Taudul
6b2abcb47a Properly set countMap to 0 when loading a trace.
The live capture code path already does the equivalent thing.
2026-07-28 23:47:49 +02:00
Bartosz Taudul
e0862e8534 Disallow implicit StringIdx construction. 2026-07-28 22:33:26 +02:00
Bartosz Taudul
ec0a1c0606 Update manual. 2026-07-28 20:52:41 +02:00
Bartosz Taudul
372b83d590 Update NEWS. 2026-07-28 20:48:14 +02:00
Bartosz Taudul
4779856174 Shorten image names in flame graph blocks. 2026-07-28 20:36:06 +02:00
Bartosz Taudul
c4b1308c21 Use image name shorten helper. 2026-07-28 20:36:06 +02:00
Bartosz Taudul
488270c728 Add image name shortening helper. 2026-07-28 20:28:27 +02:00
Bartosz Taudul
ff81feedca Add tooltip for unknown flame graph blocks. 2026-07-28 20:25:39 +02:00
Bartosz Taudul
b677b352ee Show image names in flame graph unknown items. 2026-07-28 20:09:13 +02:00
Bartosz Taudul
8b836a5d56 Display unknown frames on flame graph. 2026-07-28 19:27:30 +02:00
Bartosz Taudul
ee54c04d3b Don't display empty tooltip in no-symbol-data case. 2026-07-28 19:26:57 +02:00
Bartosz Taudul
fa8bd1adf8 Fix grouping by name when merging flame graphs. 2026-07-28 19:25:35 +02:00
Bartosz Taudul
987d440d74 Merge pull request #1435 from damcclos/damcclos/client-ordering
Ordering the clients in the connection dialog to prevent jumping in the list. Ordering by time still caused jumps.
2026-07-28 17:46:29 +02:00
David McCloskey
064d400cf1 Ordering the clients in the connection dialog to prevent jumping in the list. Ordering by time still caused jumps. 2026-07-27 15:33:14 -05:00
Bartosz Taudul
d3af48990c Change "hide unknown" check from unknown file name to unknown function name.
Knowing that function "memcpy" took a certain amount of time is very
actionable, even if the source file name (from libc) is not available.
2026-07-27 22:07:34 +02:00
Bartosz Taudul
36d93e27c4 Add a table documenting client build options to the user manual. 2026-07-27 19:58:28 +02:00
Bartosz Taudul
3a65b27838 Update NEWS. 2026-07-27 19:33:55 +02:00
Bartosz Taudul
71c98c9dfb Ignoring mem faults also ignores repeated allocation for the same address. 2026-07-27 19:33:55 +02:00
Bartosz Taudul
12fdc7d8d2 Extract recording free event in MemData. 2026-07-27 19:33:55 +02:00
Bartosz Taudul
9481319d4c Regenerate markdown manual. 2026-07-26 19:00:15 +02:00
Bartosz Taudul
3c88115926 Update NEWS. 2026-07-26 19:00:15 +02:00
Bartosz Taudul
ed5f333f90 Update manual. 2026-07-26 18:59:26 +02:00
Bartosz Taudul
a1ef58efac Remove an unreachable leaf flag ternary.
The tree node is only drawn when the count is greater than zero, so the
leaf flag could never be selected. Use the plain TreeNode call.
2026-07-26 18:59:25 +02:00
Bartosz Taudul
24b2702977 Track the maximum of combined sample costs for hotness colors.
The child calls display normalized hotness colors by the sum of two
independently tracked maxima, which can occur on different addresses.
The denominator could therefore exceed any real combined cost, so
nothing reached full heat and the color scale changed meaning when the
child calls display was toggled. Track the maximum of the per-address
sums instead.
2026-07-26 18:55:09 +02:00
Bartosz Taudul
9b3c6784fb Allow opening entry stacks for symbols with only inclusive counts.
Rows in the sampling statistics were only clickable when the symbol had
exclusive samples. In the "with children" accumulation mode this made
symbols which never directly executed, such as dispatchers or wrappers
with all cost in their callees, visible but completely inert, even
though the entry stacks window can now display how such symbols were
reached. Base the interactivity on the count shown in the current
accumulation mode. The popup menu items already disable themselves when
their data is not available.

The inline function expansion follows the same rule, so expanding such
a symbol now lists its inline functions with their inclusive counts
instead of showing an empty tree.
2026-07-26 18:55:09 +02:00
Bartosz Taudul
26d61dfdba Make find zone sample percentages relative to the matched zones.
The find zone samples list shares the drawing code with the sampling
statistics window, which computed the percentage denominator from its
own state: the whole-trace sample count, or the statistics range
filter, if one was active. The find zone counts are scoped to the
matched zones, so the percentages mixed two meanings in one table: the
time column was relative to the zone selection while the count column
was relative to the whole trace, and changing the range filter in the
statistics window silently rescaled it.

Pass the denominator from the caller. Find zone sums its zone-scoped
counts, so both columns are now relative to the selection, and the
statistics window computes the same denominator as before.
2026-07-26 18:55:09 +02:00
Bartosz Taudul
c2c1c4e2b7 Ignore the statistics range filter in the disassembly LLM tool.
The symbol disassembly tool scoped its cost data to the statistics
range filter. This is invisible UI state which the model cannot see, so
range-limited numbers were indistinguishable from whole-trace figures
and could silently change between tool calls. The other sampling tools
always report whole-trace data; do the same here.
2026-07-26 18:55:09 +02:00
Bartosz Taudul
a429242f0f Exclude context switch samples from zone call stack reconstruction.
The heuristic reconstruction picks the call stack from the samples
within the zone's time span. Context switch samples are always parked
at the scheduler, so for zones which spent their time blocked they
dominated the root selection and the reconstructed stack showed the
scheduler path instead of the zone's real call stack. A zone covering
only context switch samples now reconstructs nothing instead.
2026-07-26 18:55:09 +02:00
Bartosz Taudul
2d5e397c9f Exclude context switch samples from the sampling flame graph.
Context switch samples are excluded from the sampling statistics, but
the flame graph built from thread samples included them. Threads which
spend time blocked accumulated large scheduler towers which none of the
other sampling views show, and the flame graph totals did not
correspond to the statistics for the same trace. Filter the samples the
same way the trace load jobs do.
2026-07-26 18:55:08 +02:00
Bartosz Taudul
b743c13695 Exclude context switch samples from the LLM sampling stats total.
The per-symbol exclusive counts reported by the tool come from the
sampling statistics, which exclude context switch samples, but the
total time reference was computed from the full sample count. This
inflated the total and made every symbol look proportionally cheaper
to the model. Use the same denominator as the statistics window.
2026-07-26 18:55:06 +02:00
Bartosz Taudul
52e31be7f4 Do not touch the thread compression cache in a load-time job.
The job which builds the symbol samples and child samples maps called
CompressThread, which updates the lookup cache and can insert into the
compression map, while the timeline processing job concurrently reads
the map, deliberately using the raw lookup to avoid this exact hazard.
All threads are already present in the compression data loaded from the
trace, so use the raw lookup as well.
2026-07-25 23:02:14 +02:00
Bartosz Taudul
f51b984edb Guard against missing symbol data for the top inline name.
When displaying the hottest inline function's name in place of the base
symbol name, the symbol map lookup was dereferenced without checking
for a missing entry, crashing in release builds. Keep the base symbol
name when the inline symbol has no symbol data.
2026-07-25 23:02:14 +02:00
Bartosz Taudul
8da27d6042 Gate sampling data accessors on background processing completion.
Several code paths read the per-symbol sample lists, symbol statistics
or child sample data without checking the readiness flags. These
structures are populated by background jobs during trace load, and the
flags are the only synchronization mechanism, so reading early races
the jobs and trips the readiness asserts in debug builds. The find zone
samples list checked a different flag than the data it reads requires,
and two symbol view conditions called the accessor before the readiness
check made elsewhere in the same function. The trace information window
performed no check at all, while it is typically open during loading.
2026-07-25 23:02:13 +02:00
Bartosz Taudul
74b08cf5c6 Clarify the sampling percentage denominator comment.
The denominator includes samples with unresolved call stacks and
samples belonging to filtered-out rows, so describing it as the number
of samples attributable to the displayed symbols was overstating what
the code does.
2026-07-25 23:02:13 +02:00
Bartosz Taudul
b3de7f34fb Require symbol data for the entry stacks button in symbol view.
The button was gated on the entry stack maps being non-empty, but the
sample entry stacks window also needs the symbol data to display
anything. Without it, clicking the button silently closed the window
through its null guard. Match the gate used by the statistics window
menu items.
2026-07-25 23:02:13 +02:00
Bartosz Taudul
dd8eff0de6 Guard relative inline percentages against an empty base count.
Since the inclusive count of an aggregated symbol entry is taken from
the base symbol's own statistics, a row whose base symbol could not be
resolved can display an inclusive count of zero while its inline
functions have nonzero counts. With the relative inline display active,
the per-inline percentages divided by the base count, printing infinity
in such cases. The time percentage variant divided by the same count
scaled by the sampling period, which cancels out, so both variants can
share the guarded reciprocal.
2026-07-25 23:02:13 +02:00
Bartosz Taudul
46c70fcbb8 Do not mark sorted vectors as unsorted on equal elements.
SortedVector considered an appended element equal to the last one to
break the ordering, marking the vector as unsorted. A non-decreasing
sequence is sorted, so only a strictly smaller element has to trigger
the marker. Equal keys are common: child sample vectors receive
identical timestamps whenever a recursive call stack contains the same
call site twice, which flipped the vectors to unsorted on virtually
every recursive workload and caused the lazy sort in GetChildSamples to
run over and over again while holding the data lock.

The lazy sort machinery handles duplicate keys correctly, as both the
prefix and tail merge windows are computed with lower bounds.
2026-07-25 23:02:13 +02:00
Bartosz Taudul
97d6b5f9fa Guard sampling percentage denominators against underflow.
The denominator subtracts context switch sample counts from total
sample counts, both for the whole trace and within the range filter.
The subtraction could underflow: on traces with inconsistent sample
ordering the binary searches over the unsorted sample vectors can
return too few samples, and a sample which arrives with a duplicated
timestamp is merged into the existing entry while its context switch
classification is still recorded, so the context switch samples are not
a strict subset of the samples. An underflow made every percentage in
the table display as zero.

Clamp the subtraction, per thread in range mode, so that one thread
with bad data does not affect the counts of the others.
2026-07-25 23:02:13 +02:00
Bartosz Taudul
b68fe43335 Keep context switch sample vectors sorted.
Context switch samples were appended in arrival order. Samples which
were postponed due to missing context switch data are replayed after
newer samples were already classified, so the vector could become
unordered. Everything that reads it assumes time order: the wait stacks
range filter, the sampling statistics percentage denominator, and the
context switch sample filters in the trace load jobs. In the load jobs
an unordered vector could silently disable the filtering for the rest
of a thread, reintroducing the context switch samples into the symbol
and child sample maps.

Use a SortedVector and restore the ordering at the points where it can
break: after the postponed sample replay and when saving a trace. The
save file version is bumped, so that the sort order check on load is
only performed for traces saved by previous versions.
2026-07-25 23:02:13 +02:00
Bartosz Taudul
5563461201 Extend the symbol parents LLM tool with the reached modes.
The tool only reported "was executing" entry stacks, while the sample
entry stacks window now defaults to showing the stacks through which a
symbol was reached. The model was told there is no data for symbols
which never executed directly, even when the window right next to it
displayed their entry stacks.

Add an optional "mode" parameter with the "reached" (non-reentrant),
"reached_recursive" and "executing" values. The default matches the UI
default. Inline symbols are now accepted, as the reached maps cover
them directly, and the executing mode can use their exact statistics.
The reported mode is included in the tool output, and the chat view
shows it in the tool call label.

The tool description keeps the mode summaries short. The detailed
guidance on choosing a mode is in the optimization skill, so it only
occupies context when the skill is loaded.
2026-07-25 23:02:13 +02:00
Bartosz Taudul
851b1f80f1 Take the data lock in LLM tools which access worker state.
LLM tool calls execute on the LLM worker thread, while the render
thread holds the worker data lock for the duration of each frame and
the network thread mutates worker data under the same lock during live
capture. The tools accessed worker data with no synchronization at all.
Some of the accessors also lazily modify worker state, such as the
postponed symbol list sorting or the lazy sorting of child sample
vectors, so this raced even in a fully loaded trace.

Take the data lock in the five tools which access worker state. No LLM
locks are held while tools execute, so no lock ordering cycle with the
render thread is possible. Tools which perform network requests do not
touch worker data and remain lockless, as holding the lock across a
network transfer would stall the profiler.

The background jobs which compute sampling statistics during trace load
do not hold the data lock. The readiness flags are the synchronization
mechanism there, so gate the sampling tools on them. This also matches
the readiness asserts in the worker accessors these tools call.
2026-07-25 23:02:10 +02:00
Bartosz Taudul
507f2ae8b5 Update NEWS. 2026-07-25 19:46:02 +02:00
Bartosz Taudul
941669c1c6 Base sampling statistics percentages on real sample counts.
With an active range filter, the percentage denominator was a
theoretical capacity estimate: the number of samples a single thread
would produce if it ran continuously through the range at the nominal
sampling rate. Actual sample volume scales with total CPU occupancy
across all threads, so the estimate was off in either direction: on
multi-core workloads percentages were inflated several times over and
could exceed 100%, while on mostly idle workloads they were deflated.
Toggling the range filter also silently changed what the percentages
meant, as the whole-trace mode divides by the collected sample count.

Count the samples actually present in the range instead, and exclude
context switch samples from both denominators, as they cannot be
attributed to any displayed symbol. Percentages now mean the same thing
with and without a range filter: the share of attributable samples.

Time percentages are intentionally unchanged. They are normalized by
wall clock time in both modes, where exceeding 100% legitimately means
more than one core was busy.
2026-07-25 19:46:01 +02:00
Bartosz Taudul
c880163f96 Sort child sample vectors lazily.
During live capture, child samples were appended to their per-address
vectors in arrival order. Samples postponed due to missing context
switch data are replayed after newer samples were already processed, so
the vectors could become unordered. All range-limited queries binary
search these vectors by time and would silently return wrong results.
Inserting in sorted order at collection time would require a mid-vector
insertion for every stack frame of every replayed sample, so instead
the vectors are now SortedVector and are sorted lazily when accessed,
following what the inline symbol list already does. This also restores
proper query results for traces with inconsistent sample order.
2026-07-25 19:46:01 +02:00
Bartosz Taudul
3293e263c6 Exclude context switch samples from symbol and child sample maps.
Context switch samples are excluded from sampling statistics, as they
are not produced by the statistical profiling timer and their stacks
are always parked at the scheduler. During live capture this exclusion
is structural, as such samples never enter the statistics processing
path. On trace load, however, only the job which computes symbol
statistics and the instruction pointer map filtered them out. The job
which builds the per-symbol sample lists and the child sample map did
not, so on a loaded trace these two structures included tens of
thousands of context switch samples that a live session would not
count.

This made range-limited statistics counts exceed the whole-trace
counts, inflated child sample costs in the symbol view, and caused the
same trace to show different numbers live and after a save and reload.
On the test trace, __schedule reported 56 exclusive samples but 32747
entries in its sample list.

Apply the same context switch sample filter when building the symbol
samples and child samples maps.
2026-07-25 19:46:01 +02:00
Bartosz Taudul
6063e22bc7 Fix inclusive counts of symbols with inlined functions.
When inline functions are aggregated into their base symbols in the
sampling statistics, the inclusive counts were summed up, the same as
the exclusive counts. This is wrong. Exclusive counts partition the
samples, as each sample is attributed to exactly one symbol. Inclusive
counts overlap: an inline function can only ever be on the stack within
a frame group whose base is its parent symbol, so every sample counted
for an inline function is also counted for the base symbol. Summing
therefore counted the same sample multiple times, inflating the counts,
the sort order and the displayed percentages, in the worst observed
case by a factor of 7.

The base symbol's own inclusive count is exactly the correct value for
the aggregated entry, so use it directly.
2026-07-25 19:46:01 +02:00
Bartosz Taudul
8711f87cb5 Disable entry stacks menu item when there is no data to show.
The statistics window can display rows for which the sample entry
stacks window cannot show anything: symbols with no symbol data,
symbols that only have range-mode sample counts without callstack
statistics, or any row while the statistics are still being computed
in the background. Grey out the menu item in such cases, following
what the "View symbol" item already does.
2026-07-25 19:46:01 +02:00
Bartosz Taudul
34587e6b4c Only show entry stacks button in symbol view when data is available.
The button was displayed unconditionally, allowing the sample entry
stacks window to be opened for symbols without any sample data. Gate it
on the availability of callstack sample statistics and a non-empty
reached map, following what the call stack window does. Checking for
statistics readiness also prevents asserting when the button is used
while a trace is still being loaded in the background.
2026-07-25 19:46:01 +02:00
Bartosz Taudul
d5760a05ae Do not crash on missing symbol data in sample entry stacks window.
The window can be opened for a symbol which has no symbol stats or no
symbol data. For example, the statistics window can produce clickable
rows for symbols that are not present in the symbol map. Close the
window instead of dereferencing null pointers.
2026-07-25 19:46:01 +02:00
Bartosz Taudul
bacfbccb97 Use reached stats for entry stacks access in assembly view.
The instruction tooltip advertised the number of "was executing" entry
call stacks, but middle-clicking opens the sample entry stacks window,
which now defaults to the "was reached" mode. Count the non-reentrant
reached stacks instead, so the tooltip matches what the window shows.

Gating on the reached map also makes entry stacks accessible for
symbols which only ever appear deeper in the call stack. This can
happen when inspecting instructions whose cost consists purely of
child samples.
2026-07-25 19:45:58 +02:00
Bartosz Taudul
2e9e0e2643 Update NEWS. 2026-07-25 17:33:00 +02:00
Bartosz Taudul
fc26cc0f79 Update manual. 2026-07-25 17:30:32 +02:00
Bartosz Taudul
849bc3ae8c Allow entry stacks for symbols that were only reached. 2026-07-25 17:28:07 +02:00
Bartosz Taudul
67ae7d849f Add "was reached" modes to sample entry stacks window. 2026-07-25 17:25:17 +02:00
Bartosz Taudul
1dfcacd802 Collect entry stacks for "was this symbol reached" statistics.
SymbolStats now has two additional entry stack maps, which record the
call stacks below every occurrence of a symbol in a sample, not only
when the symbol was at the top of the stack:

- wasReached counts each occurrence separately. If a symbol re-enters
  itself through recursion, every re-entry adds an entry stack, which
  will itself contain the symbol somewhere below.
- wasReachedNonReentrant counts only the outermost occurrence, so each
  sample contributes exactly once and recursion is ignored. The sum of
  counts in this map equals the symbol's inclusive sample count.

To achieve this, the call stack is walked bottom-up, which makes the
first encountered occurrence of a symbol the outermost one. Symbols at
inline positions get the remaining inline frames of their frame group
as a synthetic frame, mirroring what was already done for the top of
the stack.

The wasExecuting and wasExecutingBase maps are now filled by the same
walk, as its topmost-frame special case. The keys they receive are
identical to what the previous code produced.

Note that there is no "base" variant of the reached maps. A base symbol
is present as the last frame of every frame group that contains its
inline functions, so its wasReached map already covers the whole symbol
at base granularity. Merging in the inline symbols' maps, as done for
wasExecutingBase, would only multi-count the same samples.
2026-07-25 17:02:42 +02:00
Bartosz Taudul
2e23c112bd Extract synthetic call stack interning into a helper. 2026-07-25 16:57:57 +02:00
Bartosz Taudul
d74d77226c Guard against empty synthetic call stacks in parents frame trees.
A sample with a single-frame call stack already produces a zero-length parent
stack, and GetParentsCallstackFrameTree{BottomUp,TopDown} call cs.back() /
cs.front() on it, resulting in UB.
2026-07-25 16:43:43 +02:00
Bartosz Taudul
8f52e0e5f5 Enable clipboard on macOS. 2026-07-25 14:34:24 +02:00
Bartosz Taudul
9db60de03b Do not embed default ImGui font. 2026-07-25 14:30:13 +02:00
Bartosz Taudul
e53c2849fa Bump ImGui to 1.92.9-docking. 2026-07-25 14:29:12 +02:00
Bartosz Taudul
7c30fedaaa Rebuild markdown manual. 2026-07-25 00:19:47 +02:00
Bartosz Taudul
9314b8cbce Update manual. 2026-07-25 00:17:42 +02:00
Bartosz Taudul
ff3e31f564 Update desktop file. 2026-07-24 23:14:09 +02:00
Bartosz Taudul
ac159d6127 Fix RUNPATH in tracy-profiler AppImage
patchelf the stripped binary to /../lib, collapsing the RUNPATH
to the single intended entry. The wayland .pc files baked an absolute
build-host path and a trailing empty component into RUNPATH; the former
leaks the build environment and could shadow bundled libs on the target,
the latter can cause cwd library searches on older glibc.

Add patchelf to the appimage workflow's apt install list.
2026-07-24 20:34:10 +02:00
Bartosz Taudul
bcddc1d9f1 Cache wayland checkout. 2026-07-24 20:34:08 +02:00
Bartosz Taudul
d231c2e95f Correct way to link with wayland libraries. 2026-07-24 18:25:05 +02:00
Bartosz Taudul
b94a1e22c1 Update git ignore list. 2026-07-24 18:25:05 +02:00
Bartosz Taudul
cf82fb9bb0 Disable unused stuff if building libcurl. 2026-07-24 18:25:05 +02:00
Bartosz Taudul
f30d32a908 Add AppImage build script and workflow. 2026-07-24 18:25:05 +02:00
Bartosz Taudul
b7eda34332 Add a warning about minimum supported wayland-client version. 2026-07-24 18:11:42 +02:00
Bartosz Taudul
5036ec9128 Merge pull request #1434 from damcclos/damcclos/thread-lock-time
Add function to calculate thread lock wait time in TracyTimelineItemThread
2026-07-23 22:14:46 +02:00
David McCloskey
59a8ae2d7d Adding documentation 2026-07-23 14:38:56 -05:00
David McCloskey
ea3d0b4d8d Add function to calculate thread lock wait time in TracyTimelineItemThread
This update introduces a new function, GetThreadLockWaitTime, which computes the total wait time for locks held by a specific thread. The function aggregates wait times across all locks and updates the tooltip to display lock wait time and count, enhancing the profiling capabilities of the timeline item.
2026-07-23 14:21:54 -05:00
Bartosz Taudul
917c4baffb GLFW from brew should no longer be needed on macOS. 2026-07-23 21:11:07 +02:00
Bartosz Taudul
faab64018a Update NEWS. 2026-07-23 21:03:07 +02:00
Bartosz Taudul
fb296ba1a4 Update manual. 2026-07-23 21:03:06 +02:00
Bartosz Taudul
877efe4eda Track current personality and warn when the selected one is not active. 2026-07-23 20:34:59 +02:00
Bartosz Taudul
90c6ad80e1 Clamp personality index to personality table size. 2026-07-23 20:29:26 +02:00
Bartosz Taudul
67c4102bf3 Restore disabling fast model selection during replies. 2026-07-23 20:26:41 +02:00
Bartosz Taudul
c1fa33a875 Link with system libcurl to avoid libssh deps. 2026-07-23 15:43:59 +02:00
Bartosz Taudul
1d6f43d92f Disable external libraries used by freetype.
Not needed for the current set of fonts.
2026-07-23 15:30:39 +02:00
Bartosz Taudul
c2b489a0ff Do not link macOS bundle with homebrew libraries.
Do not want dependency on something being installed via brew.
2026-07-23 14:57:45 +02:00
Bartosz Taudul
7986dc18d0 Add yalter's guadec presentation to list of videos. 2026-07-23 01:53:54 +02:00
Bartosz Taudul
f9413c268d Bump usearch to 2.26.0. 2026-07-22 22:35:42 +02:00
Bartosz Taudul
6e23d220d7 Bump capstone to 6.0.0-Alpha10. 2026-07-22 22:33:29 +02:00
Bartosz Taudul
158ab87bee Rebuild markdown manual. 2026-07-22 22:04:04 +02:00
Bartosz Taudul
d2cfc7b5d1 Document LLM personality selection. 2026-07-22 21:40:01 +02:00
Bartosz Taudul
b0d75acdef Add LLM personality presets. 2026-07-22 02:10:24 +02:00
Bartosz Taudul
c482f1e25d Bury fast model deeper in the UI. 2026-07-22 01:18:47 +02:00
Bartosz Taudul
8221a80b47 Add icon to LLM API entry field. 2026-07-22 01:13:20 +02:00
Bartosz Taudul
afe249b6d3 Do not disconnect active connections.
The timeout option stops the connection if it has now finished after the
specified time, even if data transfer is underway. Switch to using a low
speed timeout instead, which stops the connection only if no data is being
transferred.

A typical case where this was hit was a slow-responding LLM generating
a large amount of text.

The high timeout value is still needed to account for long prefill times.
2026-07-21 23:53:01 +02:00
Bartosz Taudul
72b1edc39d Set executable bit in macOS bundle. 2026-07-21 20:24:38 +02:00
Bartosz Taudul
2247f1d14b Add macOS artifacts to release workflow. 2026-07-21 19:57:45 +02:00
Bartosz Taudul
b0be3a5776 Bump actions/setup-python from 2 to 7. 2026-07-21 19:52:22 +02:00
Bartosz Taudul
dc261fe799 Add permissions contents write to release workflow. 2026-07-21 19:46:37 +02:00
Bartosz Taudul
21f0d15f5f Do not install library in CI workflows. 2026-07-21 19:35:43 +02:00
Bartosz Taudul
51f2b1da87 Do not zip bundle. 2026-07-21 19:21:33 +02:00
Bartosz Taudul
b32d454bcd Try making a macos bundle. 2026-07-21 16:42:33 +02:00
Bartosz Taudul
fb4dcd4e96 Switch macos CI runner to macos-latest. 2026-07-21 15:04:37 +02:00
Bartosz Taudul
d67cf385ff Merge pull request #1431 from alandtse/fix/mcp-live-worker-lock-race
fix(mcp): lock Worker reads vs receive thread
2026-07-20 12:04:36 +02:00
Bartosz Taudul
4ffc8b0dd2 Merge pull request #1430 from alandtse/fix/d3d11-zero-frequency-divide
fix(d3d11): guard collector against zero frequency
2026-07-20 12:04:07 +02:00
Alan Tse
5f2d14338e docs(mcp): fix eval-guide staleness
Two staleness gaps a cold agent would hit: get_sections()'s
documented {start, end, text} shape didn't match the actual
return value once category was added, and is_background_done()
-- introduced to close the stats-read race exercised while
testing the lock fix -- wasn't mentioned anywhere, so a cold
agent had no way to discover it short of dir(ctx).
2026-07-19 22:32:06 -07:00
Alan Tse
bfcec08e93 feat(mcp): expose background-load readiness
Smoke-testing the lock fix against a real 143M-zone capture
surfaced a second concurrent-mutation source: file loads spawn a
background thread that finishes populating zone/symbol statistics
after Worker construction returns, so get_all_zone_stats() et al.
could silently come back empty or partial with no way to tell
"not built yet" from "genuinely empty".

Adds is_background_done (a relaxed atomic, same as is_connected,
so no locked() wrapper needed) and surfaces it as background_done
in list_instances, with load_capture's docstring pointing callers
at it.
2026-07-19 22:32:04 -07:00
Alan Tse
15bc3fce1a fix(mcp): lock Worker reads vs receive thread
Every Worker-reading Python binding except save_worker() read
m_data's zones/plots/threads/etc. without taking
Worker::ObtainLockForMainThread(), racing the live receive thread
that mutates the same structures. This matches
alandtse/tracy#2: crashes only ever hit live instances, never
file-loaded ones (no concurrent writer there), and cluster right
after a big save_trace when the receive thread is catching up on
backlog.

Routes every Worker accessor through a new locked() wrapper using
the same cooperative lock TracyView.cpp already takes once per
frame, so reads can't race live writes without introducing new
stalls on the profiled client.
2026-07-19 22:32:04 -07:00
Alan Tse
dc9dfb2e65 fix(d3d11): guard collector against zero frequency
D3D11_QUERY_DATA_TIMESTAMP_DISJOINT can report Frequency == 0 even
when Disjoint == FALSE. Neither the initial CPU/GPU calibration
loop nor the per-frame Collect() path guarded against this, so
`timestamp * (1000000000 / disjoint.Frequency)` divides by zero
and crashes the profiled application with
EXCEPTION_INT_DIVIDE_BY_ZERO inside client instrumentation code.

Treats a zero frequency the same as an existing guard already does
for a disjoint result: skip/retry in calibration, and in Collect()
advance the checkpoint and drop the batch via TracyD3D11Panic,
matching the existing disjoint-timestamp handling immediately
above it.
2026-07-19 22:28:52 -07:00
Bartosz Taudul
ef099b0555 The UD- prefix is problematic in router mode. 2026-07-19 01:17:52 +02:00
Bartosz Taudul
956e082e5b ResetGraph lambda can't be static. 2026-07-18 15:33:43 +02:00
Bartosz Taudul
be6377f81b Constrain source view window size. 2026-07-18 14:51:41 +02:00
Bartosz Taudul
249b76f175 Do not display time, reason data for aggregate wait stacks. 2026-07-18 14:39:52 +02:00
Bartosz Taudul
6a916a6c9f Constrain zone info window size. 2026-07-18 14:31:15 +02:00
Bartosz Taudul
0f56c8ac7e Constrain callstack window size. 2026-07-18 14:19:33 +02:00
Bartosz Taudul
8a4876a071 Fix wrong formatting. 2026-07-18 14:15:14 +02:00
Bartosz Taudul
7539daa602 Constrain sample entry stacks window size. 2026-07-18 14:11:33 +02:00
Bartosz Taudul
ab46247a9c Constrain frame statistics window size. 2026-07-18 14:07:48 +02:00
Bartosz Taudul
7bc32a1536 Use sub-child in frame statistics window. 2026-07-18 14:07:22 +02:00
Bartosz Taudul
6858a664dc Constrain wait stacks window size. 2026-07-18 14:01:29 +02:00
Bartosz Taudul
3eae7a6d65 Constrain LLM window size. 2026-07-18 13:52:58 +02:00
Bartosz Taudul
64c0c5c85f Constrain compare window size. 2026-07-18 13:46:24 +02:00
Bartosz Taudul
74e1ddfbeb Constraint window memory size. 2026-07-18 13:42:14 +02:00
Bartosz Taudul
f472b92f13 Constraint statistics window size. 2026-07-18 13:40:20 +02:00
Bartosz Taudul
6c8dd9fd93 Constrain find zone window size. 2026-07-18 13:37:10 +02:00
Bartosz Taudul
8184964c2d Include sub-window padding and scrollbars. 2026-07-18 13:36:45 +02:00
Bartosz Taudul
eef8fac58b Constrain messages window size. 2026-07-18 13:21:35 +02:00
Bartosz Taudul
2d2487e2e0 Constrain flame graph window size. 2026-07-18 13:20:15 +02:00
Bartosz Taudul
562fea5564 Add DPI change notification method in View. 2026-07-18 13:17:42 +02:00
Bartosz Taudul
3608d0c271 Add imgui window size constraints helper. 2026-07-18 13:17:42 +02:00
Bartosz Taudul
f08792f217 Remove filtering for things that are no longer present. 2026-07-18 12:50:47 +02:00
Bartosz Taudul
87b838f553 Update manual. 2026-07-18 12:32:50 +02:00
Bartosz Taudul
ed39de8025 Update NEWS. 2026-07-18 12:23:14 +02:00
Bartosz Taudul
84c6548c94 Add option to group symbol flame graph items by name, not symaddr. 2026-07-18 12:20:38 +02:00
Bartosz Taudul
0b2e7dd13c Add StringIdx equality comparison operator. 2026-07-18 12:20:32 +02:00
Bartosz Taudul
eb12b62af6 Extract flame graph reset functionality. 2026-07-18 12:07:43 +02:00
Bartosz Taudul
91bababef7 Common path for both trees in sample entry stacks. 2026-07-17 22:24:58 +02:00
Bartosz Taudul
f13bb0bf52 Fix UI logic. 2026-07-15 02:09:34 +02:00
Bartosz Taudul
0caee2147c Update manual. 2026-07-15 01:20:14 +02:00
Bartosz Taudul
3e91ee15b1 Display FPS time range values in frame compare. 2026-07-14 23:56:28 +02:00
Bartosz Taudul
f7c63ac40c Display single FPS value in frame stats tooltip, if min and max are equal. 2026-07-14 23:55:44 +02:00
Bartosz Taudul
3d85d0468e Remove unneeded capture. 2026-07-14 21:28:33 +02:00
Bartosz Taudul
ff3c57c8d3 Category may exist while sections are not filled.
Dereference of end() iterator in section category options list
(profiler/src/profiler/TracyView_Options.cpp:152-153)

The new category list iterates categories = m_worker.GetSectionDescriptions()
(the description map) and then looks up each category in sections = m_worker.
GetSections():

for( const auto& v : categories )
{
    ...
    auto it = sections.find( v.first );
    ImGui::TextDisabled( "(%s)", RealToString( it->second.size() ) );
    ...
}

sectionsDescription is a superset of sections's keys: ProcessSectionSetup
(server/TracyWorker.cpp:7484-7496) inserts into sectionsDescription only,
while ProcessSectionEnter (server/TracyWorker.cpp:7416-7454) inserts into
both. A category set up via TracySectionSetup that has no TracySectionEnter
events yet (or a saved trace containing such a category) will be in
sectionsDescription but not in sections. find returns end() and
it->second.size() is undefined behavior (likely crash).
2026-07-14 21:21:45 +02:00
Bartosz Taudul
a501117392 Use proper data size. 2026-07-14 21:20:00 +02:00
Bartosz Taudul
916e62de17 Display count of sections in each category. 2026-07-14 19:17:22 +02:00
Bartosz Taudul
88c491e0f9 Add select/unselect all buttons to section categories list. 2026-07-14 19:14:55 +02:00
Bartosz Taudul
1a6b7781bd Update manual. 2026-07-14 19:13:01 +02:00
Bartosz Taudul
71810c4265 Update NEWS. 2026-07-14 19:13:01 +02:00
Bartosz Taudul
15fecdbf79 Display section category in section tooltips. 2026-07-14 19:13:01 +02:00
Bartosz Taudul
c1e8c7bac9 Implement disabling specific section categories. 2026-07-14 19:13:00 +02:00
Bartosz Taudul
03af70fd78 Add section categories descriptions accessor. 2026-07-14 19:13:00 +02:00
Bartosz Taudul
7933c00447 Add notification for when sections are hidden. 2026-07-14 19:13:00 +02:00
Bartosz Taudul
eb53d28358 Repurpose "limit to view" icon for sections. 2026-07-14 19:13:00 +02:00
Bartosz Taudul
2e0367cd1f Implement disabling drawing of sections. 2026-07-14 19:13:00 +02:00
Bartosz Taudul
c2fa2d4d7b New section selection menu in compare window. 2026-07-14 19:13:00 +02:00
Bartosz Taudul
f86105c600 New section range copy in playback. 2026-07-14 19:13:00 +02:00
Bartosz Taudul
2b9e7a53ff Further extraction of common functionality. 2026-07-14 19:13:00 +02:00
Bartosz Taudul
172f32d075 Use section categories in dyna example. 2026-07-14 19:12:59 +02:00
Bartosz Taudul
7dab948434 List sections and categories in range limits window. 2026-07-14 19:12:59 +02:00
Bartosz Taudul
e49b96ec03 Section items lister. 2026-07-14 19:12:59 +02:00
Bartosz Taudul
42fc8d8d8f Add getter for section category name. 2026-07-14 19:12:59 +02:00
Bartosz Taudul
ece976e820 Move Range, RangeSlim from TracyViewData.hpp to TracyUtility.hpp. 2026-07-14 19:12:59 +02:00
Bartosz Taudul
a8d5f12f16 Properly process section categories.
Section selection UI disabled for now.
2026-07-14 19:12:59 +02:00
Bartosz Taudul
aacc82df3d Hook up server-side no-op section setup. 2026-07-14 19:12:59 +02:00
Bartosz Taudul
3e1de56e15 Add section category to section enter message. 2026-07-14 19:12:59 +02:00
Bartosz Taudul
859709a98a Add client-side section category setup. 2026-07-14 19:12:58 +02:00
Bartosz Taudul
50c3c89c16 Rename parents / baseParents maps to wasExecuting / wasExecutingBase.
The current "parents" mode of operation is "was this symbol executing".
The new name for the maps reflects that.

There should also be "was this symbol reached" (both recursive and non-
reentrant) modes.
2026-07-12 16:38:42 +02:00
Bartosz Taudul
60ce6fb25e Rename parent call stacks to synthetic call stacks. 2026-07-12 16:30:16 +02:00
Bartosz Taudul
9ec0535e03 Extract parent frame interning function. 2026-07-12 16:19:08 +02:00
Bartosz Taudul
81ccf5149a Increase code legibility. 2026-07-12 16:02:28 +02:00
Bartosz Taudul
7b36ab9793 Regenerate markdown manual. 2026-07-11 16:22:09 +02:00
Bartosz Taudul
2b49e91804 Update manual. 2026-07-11 16:21:55 +02:00
Bartosz Taudul
6bd6483181 Update NEWS. 2026-07-11 16:00:42 +02:00
Bartosz Taudul
22a5a5fb78 Add copy message context menu to zone info message list. 2026-07-11 16:00:42 +02:00
Bartosz Taudul
580a43c05f Enable copying messages to clipboard. 2026-07-11 15:38:12 +02:00
Bartosz Taudul
3b2d97ab21 Set filename when saving a trace to disk. 2026-07-10 20:34:22 +02:00
Bartosz Taudul
b76468885b Show callstack lines in memory plot tooltips. 2026-07-10 20:34:22 +02:00
Bartosz Taudul
c35e0f07c5 Draw longer callstack lines in memory alloc window. 2026-07-10 20:34:19 +02:00
Bartosz Taudul
7979404a8f Merge pull request #1428 from damcclos/damcclos/flame-thread-sort
Enhance Flame Graph View with Sorting Functionality
2026-07-10 20:34:09 +02:00
David McCloskey
7d1a37de2d Pulling implementation in to a separate function. 2026-07-10 12:00:48 -05:00
Bartosz Taudul
0350df11f8 Hook up section enter / leave to Lua bindings. 2026-07-10 16:24:46 +02:00
Bartosz Taudul
59013f2a8c Add GetNextSectionId() for getting section id. 2026-07-10 16:05:37 +02:00
Bartosz Taudul
a208c3c665 Merge pull request #1427 from nicozink/fix/VulkanQueryCount
Reduce Vulkan QueryCount for Apple devices
2026-07-10 15:27:50 +02:00
David McCloskey
655b8295cf Enhance Flame Graph View with Sorting Functionality
Added a "Sort" button to the Flame Graph view, allowing users to sort threads based on group hints, thread names, and IDs. Updated the child window to always show a vertical scrollbar for better usability.
2026-07-09 12:16:17 -05:00
Nico Zink
e20c716b80 Reduce Vulkan QueryCount for Apple devices 2026-07-09 16:32:22 +01:00
Bartosz Taudul
ac4defb4f3 Merge pull request #1426 from YaLTeR/fix-manual
manual: Fix decorators in the Python listing and typo
2026-07-09 13:51:39 +02:00
Ivan Molodetskikh
c6f5cf50fb manual: Fix decorators in the Python listing and typo 2026-07-09 14:06:41 +03:00
Bartosz Taudul
96a91da63a Merge pull request #1425 from YaLTeR/desktop-tracy-profiler
.desktop: Change binary name to tracy-profiler
2026-07-09 11:55:06 +02:00
Ivan Molodetskikh
e0929e9cd8 .desktop: Change binary name to tracy-profiler
That's the current binary name.
2026-07-09 12:50:15 +03:00
Bartosz Taudul
05f620ab97 Merge pull request #1424 from YaLTeR/static-nfd
Link NFD statically
2026-07-09 11:16:56 +02:00
Ivan Molodetskikh
e40ce11ba9 Link NFD statically
Currently, the profiler will build against the git NFD, but link it as a
shared library. Since libnfd is not installed when building Tracy as a
distro package, it will end up actually depending on and running against
an older distro-provided NFD, hitting an unresolved function error due
to the newly added NFD function in git.

Other deps fetched with CPM are linked statically, avoiding this
problem.
2026-07-09 10:46:00 +03:00
Bartosz Taudul
66e5b81804 Draw rectangle encompassing CPU topology package. 2026-07-09 01:04:51 +02:00
Bartosz Taudul
d565afde82 Remove force inline for SectionEnter.
gcc breaks on this:
error: function ‘static uint32_t tracy::Profiler::SectionEnter(const char*, ...)’ can never be inlined because it uses variable argument lists
2026-07-08 18:04:01 +02:00
Bartosz Taudul
bd8246cba0 Build library / test application before building profiler tools. 2026-07-08 17:55:43 +02:00
Bartosz Taudul
2205d7972f Build dyna example in linux CI workflow. 2026-07-08 17:54:12 +02:00
Bartosz Taudul
e842bf223f Use correct type for phdr addresses. 2026-07-07 17:54:03 +02:00
Bartosz Taudul
6495aab062 Add android library CI workflow. 2026-07-07 17:53:14 +02:00
Bartosz Taudul
8555514d59 Merge pull request #1423 from YaLTeR/update-install
update: Add install() to CMakeLists.txt
2026-07-07 14:06:21 +02:00
Ivan Molodetskikh
3e0ded59a5 update: Add install() to CMakeLists.txt 2026-07-07 14:03:03 +03:00
Bartosz Taudul
ba8e4bdcbf Merge pull request #1422 from alandtse/fix/mcp-instance-eviction
fix(mcp): cap tracy_mcp instance memory growth
2026-07-07 11:21:56 +02:00
Alan Tse
9bbbec42f4 fix(mcp): cap tracy_mcp instance memory growth
tracy_mcp.py runs as a long-lived singleton shared across every MCP
client, and the `instances` dict never evicted entries — each
live_connect/load_capture materializes a full trace (zones, messages,
callstacks, memory events) with no size cap, so private bytes grow
without bound across recording sessions unless unload_capture is
called manually every time.

Add two backstops, both env-configurable: an LRU cap
(TRACY_MCP_MAX_INSTANCES, default 4) enforced on live_connect/
load_capture that never evicts a still-connected live instance, and a
periodic sweep that drops a disconnected live instance once it has
sat idle past TRACY_MCP_DISCONNECTED_TTL_S (default 1800s), keeping a
grace window for post-session analysis. list_instances now reports
connected/idle_seconds so callers can spot stale sessions, and
unload_capture/eviction now explicitly call Worker.shutdown() instead
of relying on GC. Document the lifecycle expectations (call
unload_capture proactively; treat eviction as a backstop) in
eval_guide.md.
2026-07-07 00:23:47 -07:00
Bartosz Taudul
69fead1c16 Add CMake presets for the rest of utils. 2026-07-07 01:43:43 +02:00
Bartosz Taudul
df463fcbfe Add CMake presets for profiler. 2026-07-07 01:43:43 +02:00
Bartosz Taudul
467ba60cdf Add proper compile options to dyna example. 2026-07-07 01:43:43 +02:00
Bartosz Taudul
cbbfd13fa2 Merge pull request #1421 from YaLTeR/sys-reg-h
Change musl bits/reg.h to sys/reg.h
2026-07-06 18:55:20 +02:00
Ivan Molodetskikh
16a148f0be Replace __WORDSIZE with __SIZEOF_POINTER__
__WORDSIZE has been relocated from bits/reg.h (now an empty header) to
sys/reg.h in 2024:
https://git.musl-libc.org/cgit/musl/commit/?id=e709a6f07ade208ba513f9225222336f30c304b0

And in any case, it's a private definition. Let's use a common
documented define instead:
https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html

Fixes symbol resolution on musl (particularly, postmarketOS aarch64).
2026-07-06 18:57:31 +03:00
Bartosz Taudul
9131f3600b Fix copy from dialog for sections that were not ended. 2026-07-05 19:50:03 +02:00
Bartosz Taudul
b2d5d9bc52 Allow limiting playback to section scope. 2026-07-05 18:53:52 +02:00
Bartosz Taudul
ec884b2d60 Allow setting time range limit from section. 2026-07-05 18:51:58 +02:00
Bartosz Taudul
81cb4acbe6 Use ImGui mouse button identifiers instead of raw numeric ids. 2026-07-05 18:47:38 +02:00
Bartosz Taudul
f2256cf260 Add ellipsis to buttons that require further action. 2026-07-05 18:47:07 +02:00
Bartosz Taudul
ae83b00951 Add option to reset compare time range limits. 2026-07-04 22:29:14 +02:00
Bartosz Taudul
d2c69868d1 Better compare range limit time display. 2026-07-04 22:29:14 +02:00
Bartosz Taudul
18cfcd7e90 Add copy from section to compare range limits. 2026-07-04 22:29:14 +02:00
Bartosz Taudul
71d88fd198 Annotations may have the same name. 2026-07-04 22:29:14 +02:00
Bartosz Taudul
17bbc4049a Cosmetics for compare range limit. 2026-07-04 22:29:14 +02:00
Bartosz Taudul
2b0b7293a1 Filter frames by range in compare view. 2026-07-04 22:29:14 +02:00
Bartosz Taudul
07cb1030e7 Filter zones by range in compare view. 2026-07-04 22:29:13 +02:00
Bartosz Taudul
af8dc30912 Add sumSq cache to compare data for range filtering. 2026-07-04 17:41:18 +02:00
Bartosz Taudul
8f2bbc6116 Display message when no data to compare. 2026-07-03 15:24:47 +02:00
Bartosz Taudul
f01607714e Reset data when changing range limits. 2026-07-03 15:14:21 +02:00
Bartosz Taudul
533e1af389 Set time range limits in compare window from annotations. 2026-07-02 17:40:35 +02:00
Bartosz Taudul
25139748b2 Add annotations accessor to UserData. 2026-07-02 17:40:09 +02:00
Bartosz Taudul
853fff614e Support nfd parent window with wayland backend. 2026-07-02 02:14:04 +02:00
Bartosz Taudul
5be36d732b Bump nfd to current master. 2026-07-02 01:29:50 +02:00
Bartosz Taudul
836bf9d2eb Pass parent window handle to file dialogs. 2026-07-02 01:26:58 +02:00
Bartosz Taudul
db383f738b Store window handle for file selector. 2026-07-02 01:26:58 +02:00
Bartosz Taudul
a8c642f42b Cosmetics. 2026-07-02 01:01:09 +02:00
Bartosz Taudul
641bd09b6f Use extended-params version of NFD functions. 2026-07-02 00:46:30 +02:00
Bartosz Taudul
280af2e7bd Bump curl. 2026-07-02 00:30:53 +02:00
Bartosz Taudul
83298a3459 Bump usearch. 2026-07-02 00:30:48 +02:00
Bartosz Taudul
21f9c00e9f Bump md4c to current master. 2026-07-02 00:24:57 +02:00
Bartosz Taudul
b0a094bb42 Update manual. 2026-07-01 18:26:29 +02:00
Bartosz Taudul
70a09f5663 Update NEWS. 2026-07-01 17:58:05 +02:00
Bartosz Taudul
467be67807 Adjust colors. 2026-07-01 17:33:29 +02:00
Bartosz Taudul
80476da597 Color-code difference between values in comparison window. 2026-07-01 17:33:29 +02:00
Bartosz Taudul
d94d8f11ac Show P75, P90, P99, P99.9 in comparison window. 2026-07-01 17:33:29 +02:00
Bartosz Taudul
da495be8de Remove separation in "speedup or slowdown" label. 2026-07-01 17:33:28 +02:00
Bartosz Taudul
fef901f595 Show comparison of standard deviation in compare window. 2026-07-01 17:33:28 +02:00
Bartosz Taudul
74650af7a0 Show coefficient of variation in frames compare. 2026-07-01 17:33:28 +02:00
Bartosz Taudul
e740bc652c Display FPS values for mean and median in frames comparison. 2026-07-01 17:33:28 +02:00
Bartosz Taudul
7da72e51d2 Compare FPS ranges for frames. 2026-07-01 17:33:28 +02:00
Bartosz Taudul
ac5682bc75 Use boolean to differentiate between zones / frames comparison mode. 2026-07-01 17:33:27 +02:00
Bartosz Taudul
570a06dced Be more accurate about normalization behavior. 2026-07-01 17:33:24 +02:00
Bartosz Taudul
00c079cec9 Put summary updates under a lock. 2026-07-01 02:10:52 +02:00
Bartosz Taudul
80a1772d0b Separate external frames visibility for wait stacks (enabled by default). 2026-07-01 02:03:28 +02:00
Bartosz Taudul
f65949cc26 Move callstack table params to a struct. 2026-07-01 01:53:08 +02:00
Bartosz Taudul
b40ef0ff29 Draw timeline thread label after message markers to make it readable. 2026-07-01 01:26:26 +02:00
Bartosz Taudul
aca0e39190 Do not summarize very short llm queries. 2026-06-30 21:06:43 +02:00
Bartosz Taudul
ba9388cd9f Merge pull request #1404 from wolfpld/slomp/gl_query_result_no_wait
Possibly faster GL timestamp query result check
2026-06-30 21:03:40 +02:00
Bartosz Taudul
1f2190a44a Merge pull request #1420 from YaLTeR/flame-graph-pan
Fix "quantization" of sampling flame graph horizontal panning
2026-06-30 19:52:49 +02:00
Ivan Molodetskikh
b11fa7caa2 Store flame graph time as time instead of samples
Practically, this fixes horizontal panning being "quantized" to 100 µs
steps (one period). I guess it also simplifies the code a bit and makes
"time" actually mean time.
2026-06-30 19:54:54 +03:00
Bartosz Taudul
ed1a4b06b6 No cumulate time for frames. 2026-06-30 16:57:19 +02:00
Bartosz Taudul
d34659f57b Remove tree node for histogram in compare window. 2026-06-30 16:44:54 +02:00
Bartosz Taudul
a67a29e9cd Use tabs in compare window. 2026-06-30 16:39:06 +02:00
Bartosz Taudul
dc4b51e522 Always draw separator in compare window. 2026-06-30 14:22:46 +02:00
Bartosz Taudul
0a0cd27db4 Update manual. 2026-06-29 17:20:10 +02:00
Bartosz Taudul
472c84ac78 Implement coverage requirement in GetPlaybackFrameRangeFromTime. 2026-06-29 17:16:27 +02:00
Bartosz Taudul
bde9e50c84 Use binary search in GetPlaybackFrameRangeFromTime(). 2026-06-29 17:02:51 +02:00
Bartosz Taudul
dba0b6263e Add button for resetting frame image playback speed. 2026-06-29 16:32:59 +02:00
Bartosz Taudul
27f4e00aea Add require coverage checkbox. 2026-06-29 16:30:21 +02:00
Bartosz Taudul
6d6a4beda3 Add button for resetting frame image playback range. 2026-06-29 16:18:58 +02:00
Bartosz Taudul
bbb43549a6 Move playback "copy from" to a button with a popup menu. 2026-06-29 16:18:58 +02:00
Bartosz Taudul
83a703640c Add limits button to frame statistics window. 2026-06-29 16:18:58 +02:00
Bartosz Taudul
7b4a128080 Use full size button for flame graph reset view. 2026-06-29 16:18:57 +02:00
Bartosz Taudul
547891f044 Update manual. 2026-06-29 16:18:57 +02:00
Bartosz Taudul
e4acd2db77 Add icon for the "limit to view" button in time ranges window. 2026-06-29 16:18:57 +02:00
Bartosz Taudul
e7bd441ebb Hide time range copy sources in a popup menu. 2026-06-29 16:18:57 +02:00
Bartosz Taudul
4cd6c38932 Include exclusive sample count in inclusive sample count. 2026-06-27 16:44:26 +02:00
Bartosz Taudul
331881ca36 Fix counting of "with children" sample counts in recursive cases. 2026-06-27 15:00:42 +02:00
Bartosz Taudul
9aa48a62cb Fix remains of old font awesome markup. 2026-06-27 15:00:42 +02:00
Bartosz Taudul
50982fe7a3 Update manual. 2026-06-27 15:00:42 +02:00
Bartosz Taudul
549226751e Update NEWS. 2026-06-27 15:00:42 +02:00
Bartosz Taudul
6921fbfdf8 Color sample markers according to sample type. 2026-06-27 15:00:41 +02:00
Bartosz Taudul
90d4463ef0 Determine sample type. 2026-06-27 11:55:00 +02:00
Bartosz Taudul
323659d4f7 Merge pull request #1416 from YaLTeR/fix-elf-zstd-rle-mode
libbacktrace: Fix missing table assignment in RLE seq decode
2026-06-27 10:57:45 +02:00
Ivan Molodetskikh
279f7ba0df libbacktrace: Fix missing table assignment in RLE seq decode
Fixes segfault when decompressing mesa debuginfo on Aeryn.
2026-06-27 11:05:12 +03:00
Bartosz Taudul
0b120ba84f Merge pull request #1415 from wolfpld/slomp/webgpu-logging
WebGPU: more informative errors
2026-06-26 23:09:58 +02:00
Bartosz Taudul
04a8ba4c2b Update manual. 2026-06-26 20:21:03 +02:00
Bartosz Taudul
576d03e414 Update NEWS. 2026-06-26 20:21:03 +02:00
Bartosz Taudul
88702dad39 Allow setting frame image playback limit to an annotation of range limit. 2026-06-26 20:21:03 +02:00
Bartosz Taudul
30572e4810 Don't crash on annotation with no name. 2026-06-26 20:21:03 +02:00
Bartosz Taudul
57c143c9b7 Translate time range to frame image range. 2026-06-26 20:21:03 +02:00
Marcos Slomp
2221138299 more informative errors 2026-06-26 10:40:48 -07:00
Bartosz Taudul
4bc37ccb4d Allow limiting frame image playback to given frame range. 2026-06-26 19:39:24 +02:00
Bartosz Taudul
e9ad49a846 Introduce abstraction of frame image begin and end. 2026-06-26 19:07:47 +02:00
Bartosz Taudul
64a43ef3d3 Add looping to frame image playback. 2026-06-26 18:37:54 +02:00
Bartosz Taudul
8aae924d51 Make play button disabled if playback is at the end. 2026-06-26 17:46:58 +02:00
Bartosz Taudul
9370d7a379 Unify entry stacks icon. 2026-06-26 14:25:06 +02:00
Bartosz Taudul
e10c865fbf Merge pull request #1414 from YaLTeR/fix-elf-symbols
Fix image and symbol resolution on AerynOS and elsewhere
2026-06-26 14:19:08 +02:00
Ivan Molodetskikh
bd2106db68 libbacktrace: support compressed block with window descriptor
Some debuginfo on AerynOS has this format.
2026-06-26 10:46:33 +03:00
Ian Lance Taylor
99c13e53bf libbacktrace: support compressed block with no sequences
* elf.c (elf_zstd_decompress_frame): Support a compressed block
	with no sequences, only literals.

Fixes https://github.com/ianlancetaylor/libbacktrace/issues/164
2026-06-26 10:46:17 +03:00
Ian Lance Taylor
7fbe1d1345 libbacktrace: support multiple zstd frames
Based on patch by GitHub user ofats.

	* elf.c (elf_zstd_decompress_frame): New static function,
	broken out of elf_zstd_decompress.
	(elf_zstd_decompress): Call elf_zstd_decompress_frame in a loop.
	* zstdtest.c (test_large): Compress the file in chunks.
2026-06-26 10:41:29 +03:00
Ivan Molodetskikh
5745b489a2 callstack: Calculate end address from highest LOAD header
Headers aren't guaranteed to be sorted in final address order. For
example, from readelf -lW of a .so built with clang:

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x000230 0x000230 R   0x8
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x5e8274 0x5e8274 R   0x1000
  LOAD           0x5e8280 0x00000000005e9280 0x00000000005e9280 0x51e990 0x51e990 R E 0x1000
  LOAD           0xb06c10 0x0000000000b08c10 0x0000000000b08c10 0x031628 0x0323f0 RW  0x1000
  LOAD           0xb38240 0x0000000000b3b240 0x0000000000b3b240 0x009aa8 0x011c40 RW  0x1000
  DYNAMIC        0xb32270 0x0000000000b34270 0x0000000000b34270 0x000410 0x000410 RW  0x8
  GNU_RELRO      0xb06c10 0x0000000000b08c10 0x0000000000b08c10 0x031628 0x0323f0 R   0x1
  GNU_EH_FRAME   0x4e8360 0x00000000004e8360 0x00000000004e8360 0x029cf4 0x029cf4 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0
  NOTE           0x000270 0x0000000000000270 0x0000000000000270 0x000024 0x000024 R   0x4
2026-06-26 10:41:29 +03:00
Bartosz Taudul
7e2e1829c9 Update manual. 2026-06-25 18:12:21 +02:00
Bartosz Taudul
1ca38e2b70 Update NEWS. 2026-06-25 18:12:03 +02:00
Bartosz Taudul
1a420d0f5e Add frame statistics limit range. 2026-06-25 17:58:34 +02:00
Bartosz Taudul
7eae48c1a8 Make range stripes more visible when changing the range size. 2026-06-25 17:58:34 +02:00
Bartosz Taudul
1c712e41d9 Allow making annotations from ranges. 2026-06-25 17:58:34 +02:00
Bartosz Taudul
a03e5e8968 Use note sticky icon for add annotation button. 2026-06-25 17:58:32 +02:00
Bartosz Taudul
3e5310cac6 Fix sigma conversion in latex -> md script. 2026-06-25 00:14:21 +02:00
Bartosz Taudul
606cd7bdaf Protect against zero frames to show. 2026-06-24 23:49:53 +02:00
Bartosz Taudul
d924b05ab7 Calculate square of averages with double precision. 2026-06-24 23:27:36 +02:00
Bartosz Taudul
d0c7596977 Regenerate markdown manual. 2026-06-24 23:02:25 +02:00
Bartosz Taudul
0bee59ff85 Update manual. 2026-06-24 23:01:34 +02:00
Bartosz Taudul
963a968a9e Update NEWS. 2026-06-24 20:37:51 +02:00
Bartosz Taudul
f700597067 Display P75, P90, P99 and P99.9 for frame statistics. 2026-06-24 20:09:03 +02:00
Bartosz Taudul
a237b009ec Use unified percentile value calculation in find zone. 2026-06-24 19:56:24 +02:00
Bartosz Taudul
5970e90233 Display coefficient of variation in frame statistics. 2026-06-24 19:32:50 +02:00
Bartosz Taudul
5fc4ae8eef Display coefficient of variation in find zone. 2026-06-24 19:30:42 +02:00
Bartosz Taudul
a35f8a738d Display standard deviation in frame statistics. 2026-06-24 19:21:40 +02:00
Bartosz Taudul
3714e36652 Frame count in frame statistics may be not full. 2026-06-24 18:56:08 +02:00
Bartosz Taudul
565f3e5a72 Add mode to frame statistics. 2026-06-24 18:48:38 +02:00
Bartosz Taudul
17720a0cd8 Keep FPS range and max counts in the same line. 2026-06-24 18:48:38 +02:00
Bartosz Taudul
a7b0907d6c Move mean and median in frame statistics to match find zone. 2026-06-24 18:48:38 +02:00
Bartosz Taudul
552159ccf6 Remove tree node for historgram in frame statistics. 2026-06-24 18:48:38 +02:00
Bartosz Taudul
7e8180a2a7 Move frame statistics from trace info to its own window. 2026-06-24 18:48:38 +02:00
Bartosz Taudul
05d34e4daa Make it possible to hide annotation. 2026-06-24 18:48:37 +02:00
Bartosz Taudul
72d45dfdad Fix display of annotations color on annotations list. 2026-06-24 18:48:35 +02:00
Bartosz Taudul
280475ff3d Iterate ranges array when drawing ranges on timeline. 2026-06-23 23:45:41 +02:00
Bartosz Taudul
922604fe6a Do not include alpha in color entries in ranges array. 2026-06-23 23:45:11 +02:00
Bartosz Taudul
5b29550ded Move ranges setup to an appropriate file. 2026-06-23 23:29:33 +02:00
Bartosz Taudul
56eb3b776f Show ranges window after setting a range via timeline right-click popup.
Without this using the popup is quite unintuitive. Setting the range
apparently does not have an effect – because ranges are only shown if the
ranges window is open (or the windows appropriate for each of the ranges).
2026-06-23 23:07:27 +02:00
Bartosz Taudul
98ad778495 Iterate ranges array to handle range adjustments by mouse. 2026-06-23 23:06:02 +02:00
Bartosz Taudul
fa4c28af8a Iterate ranges array in timeline right-click menu. 2026-06-23 23:03:26 +02:00
Bartosz Taudul
a73a733644 Popup label is now just label, do not pass it twice. 2026-06-23 22:54:09 +02:00
Bartosz Taudul
d4cceb3e0d Use ranges array in DrawRangeEntry(). 2026-06-23 22:45:45 +02:00
Bartosz Taudul
b715c0c32e Add icon to annotation tooltip. 2026-06-23 22:41:18 +02:00
Bartosz Taudul
f71620c8c8 Use ranges array in DrawRanges(). 2026-06-23 22:33:07 +02:00
Bartosz Taudul
51e467d7b9 Make ranges private in View. 2026-06-23 21:50:22 +02:00
Bartosz Taudul
eba8c1e99a Setup ranges array. 2026-06-23 21:49:15 +02:00
Bartosz Taudul
9a785976c0 Ignore zero-length sections. 2026-06-23 02:00:11 +02:00
Bartosz Taudul
711e3a7bcf Add separator between timeline headers and content. 2026-06-23 01:58:35 +02:00
Bartosz Taudul
d84a085b71 Collapse small sections. 2026-06-23 01:58:35 +02:00
Bartosz Taudul
ae38665b95 Draw sections. 2026-06-22 19:17:01 +02:00
Bartosz Taudul
800e415400 Calculate section rows content. 2026-06-22 19:17:00 +02:00
Bartosz Taudul
f1dfedffaf Instrument program sections in dyna example. 2026-06-22 15:10:38 +02:00
Bartosz Taudul
67bb927d61 Store level name in World. 2026-06-22 15:10:28 +02:00
Bartosz Taudul
415197bd5d Remove semicolon from TracySetProgramName macro. 2026-06-22 14:57:25 +02:00
Bartosz Taudul
5177e587e9 Do not include semicolons in TracySectionEnter / TracySectionLeave macros. 2026-06-22 14:56:32 +02:00
Bartosz Taudul
ee7d2b4fd8 Track memory allocations in dyna example. 2026-06-21 23:25:42 +02:00
Bartosz Taudul
1d0806c374 Add frame image capture to the dyna example. 2026-06-21 23:12:52 +02:00
Bartosz Taudul
9cc8f3752e Mark zones in dyna example. 2026-06-21 23:05:20 +02:00
Bartosz Taudul
79e0efac96 Mark frame boundaries in dyna example. 2026-06-21 22:53:31 +02:00
Bartosz Taudul
8531e879a9 Use TracyNoop to ensure profiler library is linked with executable. 2026-06-21 22:47:34 +02:00
Bartosz Taudul
89a75d5c71 Integrate Tracy into dyna example. 2026-06-21 22:47:34 +02:00
Bartosz Taudul
e9e85ca6ee Add Dyna.net example. 2026-06-21 22:47:34 +02:00
Bartosz Taudul
3a6a23ca98 Save and load sections data. 2026-06-21 00:56:32 +02:00
Bartosz Taudul
3157dfae3e Merge pull request #1412 from jensenr30/fix-signed-int-comparison-warning
Fix warning [-Wsign-compare]
2026-06-21 00:32:51 +02:00
Ryan Jensen
978b720759 Fix warning [-Wsign-compare] 2026-06-20 17:09:26 -05:00
Bartosz Taudul
77c655eb7d Merge pull request #1411 from alandtse/feat/mcp-sections-accessor
feat(mcp): expose sections accessor in eval bindings
2026-06-20 11:49:45 +02:00
Alan Tse
bee6ac566e docs(mcp): drop dangling tracy://catalog reference
eval_guide.md referenced a tracy://catalog resource that was never
registered (only tracy://prompt and tracy://eval-guide exist), so an
agent following the guide would try to read a nonexistent resource.
Remove the references; the worked snippets the catalog described are
already inlined under "Common query patterns".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016EvfzHvUsDBSAwEzTfLTtA
2026-06-19 15:51:51 -07:00
Alan Tse
6e4041b14d feat(mcp): expose sections accessor in eval bindings
Bind Worker::GetSections() as get_sections() so the TracySectionEnter /
TracySectionLeave instrumentation added to the client is reachable from
the MCP eval tool's ctx object. Returns a list of {start, end, text}
dicts with nanosecond timestamps, matching the existing list-of-dict
accessor convention. Documented in eval_guide.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016EvfzHvUsDBSAwEzTfLTtA
2026-06-19 15:44:12 -07:00
Bartosz Taudul
1706ac57ac Separate id field is not needed. 2026-06-19 20:37:49 +02:00
Bartosz Taudul
72d62bea25 Display count of sections. 2026-06-19 20:37:49 +02:00
Bartosz Taudul
0ae37172dc Add sections accessor. 2026-06-19 20:37:45 +02:00
Marcos Slomp
d1373d3f4c code hardening for GLES 2.0 2026-06-19 11:03:30 -07:00
Bartosz Taudul
9502ab728d Merge pull request #1410 from shekhirin/alexey/mcp-streamable-http
feat(mcp): default to streamable-http transport
2026-06-19 19:45:51 +02:00
Alexey Shekhirin
7dd4dbc8a5 update docs 2026-06-19 18:36:03 +01:00
Alexey Shekhirin
96c681cf61 feat(mcp): default to streamable-http transport 2026-06-19 16:42:14 +01:00
Bartosz Taudul
2052aa2ab8 Regenerate markdown manual. 2026-06-19 15:05:58 +02:00
Bartosz Taudul
977218ea04 Document that ___tracy_alloc_srcloc strings must not be null-terminated. 2026-06-19 15:05:05 +02:00
Bartosz Taudul
fcd5944d5e Better default llm config. 2026-06-19 12:36:39 +02:00
Bartosz Taudul
34395f97ed Regenerate markdown manual. 2026-06-18 21:31:03 +02:00
Bartosz Taudul
c897729b74 Use safe file names for cache. 2026-06-18 21:30:07 +02:00
Bartosz Taudul
f8208ce5ef Add quick start guide for getting Tracy Assist running. 2026-06-18 21:21:15 +02:00
Bartosz Taudul
3516c96afd Retrieve available context on llama.cpp if not available on start.
In order for context size to be available, the model has to be loaded on
the server. However, the typical flow will be to load models on demand.
2026-06-18 21:00:25 +02:00
Bartosz Taudul
3bb5027565 Add llama.cpp tokenization support. 2026-06-18 20:58:40 +02:00
Bartosz Taudul
eefc7aee4b Detect llama.cpp router mode. 2026-06-18 20:56:07 +02:00
Bartosz Taudul
3ccda1d3c4 Add embedding model heuristic to not recognized llama API providers. 2026-06-18 20:51:53 +02:00
Bartosz Taudul
802e88585a Merge pull request #1407 from shekhirin/tracy-consistent-thread-order
feat: display threads in a consistent order
2026-06-18 14:41:20 +02:00
Alexey Shekhirin
74bbe5988b Use consistent thread order in more thread pickers 2026-06-18 09:41:45 +01:00
Alexey Shekhirin
066cb89f75 Display threads in a consistent order 2026-06-18 09:41:31 +01:00
Bartosz Taudul
8c289104d6 Regenerate markdown manual. 2026-06-18 00:39:26 +02:00
Bartosz Taudul
274e453bb6 Process section enter / leave events. 2026-06-17 22:37:31 +02:00
Bartosz Taudul
d89055908b Update manual. 2026-06-17 22:37:31 +02:00
Bartosz Taudul
801a34ea66 Add trigger parameter. 2026-06-17 22:37:31 +02:00
Bartosz Taudul
0cdb6d365e Initial section enter / leave instrumentation. 2026-06-17 22:37:31 +02:00
Bartosz Taudul
e3f4c72c85 Move TRACY_ATTRIBUTE_FORMAT_PRINTF to its own header. 2026-06-17 22:37:31 +02:00
Bartosz Taudul
eeab1bf1a9 Border sizes are scaled by ScaleAllSizes() since ImGui 1.92.7. 2026-06-17 22:37:30 +02:00
Bartosz Taudul
d26176baae Merge pull request #1406 from mcourteaux/fix-charconv
Implement C++ feature check for charconv.
2026-06-17 11:37:38 +02:00
Martijn Courteaux
021aa6a9e6 Implement C++ feature check for charconv. 2026-06-17 09:27:00 +02:00
Bartosz Taudul
8b86ada40e Fix checks. 2026-06-17 01:31:56 +02:00
Bartosz Taudul
616f33ff65 Consistent wait.time checks. 2026-06-17 01:31:01 +02:00
Bartosz Taudul
023cb20ba9 Only display wait reason or state if present. 2026-06-17 00:33:00 +02:00
Bartosz Taudul
073cd266ac Use proper thread id for wait stacks.
ContextSwitchData::Thread() is something related to fibers. Pass and use
known thread id from external context instead.
2026-06-17 00:21:51 +02:00
Bartosz Taudul
2e252d3988 Fix indentation level. 2026-06-17 00:14:03 +02:00
Bartosz Taudul
77978b68ca Explain wait stacks in callstack skill. 2026-06-16 23:53:41 +02:00
Bartosz Taudul
d9939362f5 Add wait stack data to the LLM attachment. 2026-06-16 23:53:40 +02:00
Bartosz Taudul
bba05bd3ad Update manual. 2026-06-16 23:33:16 +02:00
Bartosz Taudul
83a4a13cbd Display wait stack label and tooltip in callstack window. 2026-06-16 23:28:24 +02:00
Bartosz Taudul
9e9aabe9a1 Push CallstackViewWait to View::DrawCallstackTable(). 2026-06-16 23:18:40 +02:00
Bartosz Taudul
382a887ce9 Provide default values for View::DrawCallstackTable() parameters. 2026-06-16 23:17:47 +02:00
Bartosz Taudul
fbd1c55151 Extend View::ViewCallstack() to support optional wait stack data. 2026-06-16 23:10:43 +02:00
Bartosz Taudul
398bab8041 Store wait stack data in CallstackView. 2026-06-16 23:05:59 +02:00
Bartosz Taudul
69245e751b Cosmetics. 2026-06-16 23:05:58 +02:00
Marcos Slomp
22abcd272f cosmetic: making things a bit more idiomatic 2026-06-16 11:30:21 -07:00
Marcos Slomp
32b712c120 force inline helper method 2026-06-16 11:24:47 -07:00
Marcos Slomp
704b5c4030 refactor sentinel value 2026-06-16 11:24:27 -07:00
Marcos Slomp
dd6962b42c use queryId arg, not m_tail member 2026-06-16 11:24:12 -07:00
Bartosz Taudul
d571f2bd59 Fix integer trace parameter input field size. 2026-06-16 19:17:18 +02:00
Bartosz Taudul
781938317d Bump pugixml to 1.16. 2026-06-16 18:10:38 +02:00
Marcos Slomp
16cfd25320 potentially faster timestamp query result check
Co-authored-by: Sergio Acereda <sergio.acereda@gmail.com>
2026-06-16 08:36:55 -07:00
Bartosz Taudul
f9365abe4f Do not use samplers in renderer.
Tracy requires some textures to have repeat wrapping mode set. The ImGui
implementation of samplers doesn't make it easy to achieve. Disalbe use
of samplers and rely on texture flags, as done originally.
2026-06-16 16:02:08 +02:00
Bartosz Taudul
3f91f35d59 Merge pull request #1403 from wolfpld/slomp/gl-feature-check
Add routine to check for GL features/extensions at run-time
2026-06-16 14:50:30 +02:00
Marcos Slomp
1de94aa856 add routine to check for GL features/extensions at run-time 2026-06-15 21:19:12 -07:00
Bartosz Taudul
ec1d5bd3d7 Merge pull request #1402 from wolfpld/slomp/webgpu-example-platform
Switch webgpu example to SDL3, plus patch edge-case for wgpu-native
2026-06-15 23:48:14 +02:00
Marcos Slomp
69af195c98 edge-case bug-fix (could cause wgpu-native to panic) 2026-06-15 13:14:28 -07:00
Marcos Slomp
60699c4a92 fixing win32 builds with SDL3 + WebGPU 2026-06-15 13:14:28 -07:00
Marcos Slomp
cc45cf6046 switch to SDL3 (no cmake fetch, just find_package) 2026-06-15 13:14:28 -07:00
Bartosz Taudul
62560a6429 Add 8-bit length string transfers to the protocol. 2026-06-15 20:43:55 +02:00
Bartosz Taudul
f7b4e177ff Change misleading etc1buf variable to texbuf. 2026-06-15 19:31:06 +02:00
Bartosz Taudul
084daf0516 Force inline send string strlen helpers. 2026-06-15 19:19:13 +02:00
Bartosz Taudul
a98956f2d9 Another typo. 2026-06-15 17:17:32 +02:00
Bartosz Taudul
ac6f0f88fa Actually describe the message severity levels. 2026-06-15 17:09:42 +02:00
Bartosz Taudul
33fccb3530 Typos. 2026-06-15 17:08:39 +02:00
Bartosz Taudul
45576f6972 Merge pull request #1400 from wolfpld/slomp/gl-example
adding OpenGL example (spinning triangle)
2026-06-14 21:39:54 +02:00
Marcos Slomp
17e13bc2e0 SDL2 -> SDL3 2026-06-14 12:18:06 -07:00
Marcos Slomp
ee0c73bf25 switch to SDL2 (no cmake fetch, just find_package) 2026-06-14 11:24:14 -07:00
Bartosz Taudul
343567a3f2 Regenerate markdown manual. 2026-06-14 17:31:49 +02:00
Bartosz Taudul
20b3535623 Use fancy quotes in the manual. 2026-06-14 17:31:32 +02:00
Bartosz Taudul
5298316480 Revert emscripten back to 5.0.7. There are threading problems with 6.0.0.
Specifically, click on the red power off button to go back to the welcome
screen, and the cleanup popup never goes away.
2026-06-14 16:24:13 +02:00
Bartosz Taudul
83719fb29b WASM_BIGINT is enabled by default since emscripten 4.0.0. 2026-06-14 15:17:32 +02:00
Bartosz Taudul
f7d789eddb Split emscripten link options to multiple lines. 2026-06-14 15:09:40 +02:00
Bartosz Taudul
3816b2485e Bump used emscripten version to 6.0.0. 2026-06-14 15:06:53 +02:00
Bartosz Taudul
f8aa88d522 Explicitly disable shared libs for md4c.
Fixes emscripten build.
2026-06-14 15:06:36 +02:00
Bartosz Taudul
b5ae187f76 Disable separate fast model by default. 2026-06-12 22:20:47 +02:00
Marcos Slomp
3f203806e2 X11 workaround check 2026-06-12 13:00:33 -07:00
Bartosz Taudul
15c6b49de2 Mark text embeds as TEXT. 2026-06-12 21:43:51 +02:00
Bartosz Taudul
a153f3a562 Extend Embed macro to support TEXT parameter enabling CRLF to LF conversion. 2026-06-12 21:43:12 +02:00
Bartosz Taudul
c2998310cf Add CRLF to LF conversion support to embed. 2026-06-12 21:42:45 +02:00
Bartosz Taudul
a43b74ed8f Update NEWS. 2026-06-12 21:08:33 +02:00
Bartosz Taudul
d3047f8069 Fix memory discard + callstack.
Bug (High Severity): Wrong queue type in MemDiscardCallstack

In the callstack path of MemDiscardCallstack, the wrong queue type is
sent:

  SendMemDiscard( QueueType::MemDiscard, thread, name );

Every other callstack variant correctly uses its callstack queue type
(MemAllocCallstack, MemFreeCallstack, etc.), but this one uses the
non-callstack type. The SendMemDiscard assertion at line 1026 confirms
MemDiscardCallstack is a valid value.

Impact: The callstack captured by SendCallstackSerial() will be orphaned.
The server processes the event via the non-callstack handler, leaving the
callstack serial data unconsumed, which desynchronizes the serial queue
and corrupts all subsequent events.
2026-06-12 20:30:59 +02:00
Bartosz Taudul
3804b2580a Regenerate markdown manual. 2026-06-12 19:58:06 +02:00
Bartosz Taudul
329ac6c9f1 Document memory discard macro. 2026-06-12 19:57:45 +02:00
Bartosz Taudul
a091bb4ad2 Remove "secure" variant of alloc/free.
Random crashes are not fun. Always use the "secure" code path.
2026-06-12 19:41:17 +02:00
Bartosz Taudul
86b5f43959 Provide proper test directory. 2026-06-12 19:23:28 +02:00
Marcos Slomp
39dc688340 adding Xrandr dependency 2026-06-12 08:44:46 -07:00
Marcos Slomp
832234838b better comments and messages 2026-06-12 07:33:06 -07:00
Marcos Slomp
daba5acfbc more explicit compiler warning message 2026-06-12 07:31:03 -07:00
Bartosz Taudul
07bfe3465e Merge pull request #1356 from wolfpld/slomp/tracy-webgpu
GPU: WebGPU back-end
2026-06-12 12:11:32 +02:00
Bartosz Taudul
0544440a34 Remove unused, extremely broken code. 2026-06-11 22:35:25 +02:00
Marcos Slomp
f287508772 addressing type conversion warning 2026-06-11 13:28:32 -07:00
Bartosz Taudul
f622b97436 Backdate init time when a producer token predates it.
A zone emitted from a shared object initializer runs before the
executable's constructors, so its timestamp precedes s_initTime, which
the server uses as the trace epoch (baseTime). Such a zone converts to
negative trace time and its end no longer satisfies IsEndValid(), which
excludes it from statistics reconstruction and makes it render as
never-ending.

Record the current time when a producer token is created before
s_initTime is constructed and use it as the init time, ensuring no event
timestamp precedes the trace epoch.
2026-06-11 20:05:59 +02:00
Bartosz Taudul
dfded9d55d Recover main thread producer orphaned by cross-module init order.
ELF init_priority only orders constructors within a single module. All of
a shared object's initializers run before any of the executable's, so an
instrumented dependency .so emitting a zone from its static initializer
creates the main thread producer token against the zero-initialized
s_queue. The queue constructor then resets the producer list, orphaning
that producer: every zone emitted on the main thread from that point on
is enqueued into blocks no consumer ever iterates and silently lost,
while sampling (worker thread producer) keeps working.

Re-link such a producer right after the queue is constructed. In the
common case, where nothing was emitted during shared object init, this
merely constructs the main thread token eagerly.
2026-06-11 20:05:57 +02:00
Marcos Slomp
a2555fbb33 fixing Windows/Linux build 2026-06-11 07:37:58 -07:00
Bartosz Taudul
7180ea381f Merge pull request #1401 from Lectem/fix/win32-non-desktop
`TRACY_WIN32_NO_DESKTOP` should use `GetVersionExW` explicitly.
2026-06-11 13:01:24 +02:00
Clément Grégoire
0c74658dd3 TRACY_WIN32_NO_DESKTOP should use GetVersionExW explicitly.
Since we use `RTL_OSVERSIONINFOW` we need to use W version explicitly
2026-06-11 12:06:34 +02:00
Marcos Slomp
debda1df55 scoping the GpuCtx constructor 2026-06-10 18:57:22 -07:00
Marcos Slomp
d98608b022 issue a Tracy warning message when timestamp queries are supported but not properly implemented 2026-06-10 18:52:57 -07:00
Marcos Slomp
eb88c6eba0 adding warning about TracyOpenGL usage on Apple devices 2026-06-10 18:52:09 -07:00
Marcos Slomp
e83429c926 replacing the various platform layers by RGFW 2026-06-10 18:38:48 -07:00
Bartosz Taudul
cc091a99a2 Support key modifiers on emscripten. 2026-06-10 23:39:08 +02:00
Marcos Slomp
1b207d3e2a adding OpenGL example (spinning triangle) 2026-06-10 14:14:54 -07:00
Bartosz Taudul
f89709e99e Prevent click-through when activating annotation. 2026-06-10 22:50:45 +02:00
Bartosz Taudul
a4c5f15312 Rewrite annotations drawing. 2026-06-10 22:42:05 +02:00
Bartosz Taudul
3455fd9f82 Fix regression making existing annotations non-editable after trace load. 2026-06-10 21:48:12 +02:00
Marcos Slomp
cfc046abcd refactoring requirements 2026-06-10 11:09:51 -07:00
Bartosz Taudul
9ab39d8af3 Merge pull request #1398 from Lectem/fix/concurrentqueue-and-malloc-define
WORKAROUND_malloc/free should properly route to tracy_malloc/free
2026-06-10 17:57:32 +02:00
Clément Grégoire
bfab6d03f4 WORKAROUND_malloc/free should properly route to tracy_malloc/free 2026-06-10 17:15:33 +02:00
Marcos Slomp
0d848c3042 proper device descriptor chaining 2026-06-10 08:03:18 -07:00
Marcos Slomp
54270d3fd5 move window to top when launching from console 2026-06-10 06:24:53 -07:00
Marcos Slomp
1341f98c61 cleanup 2026-06-10 06:24:32 -07:00
Marcos Slomp
6fc279eef4 more descriptive API name 2026-06-10 06:23:57 -07:00
Bartosz Taudul
66e4f5cef7 Merge pull request #1397 from MaximeCoutantSonos/master
Fixed typo inconsistency in Shared lock C api
2026-06-10 14:47:56 +02:00
MaximeCoutantSonos
7637971e9e Fixed typo inconsistency in Shared lock C api 2026-06-10 14:26:36 +02:00
Bartosz Taudul
4e3cffc4ba Add name bank related to games. 2026-06-10 01:34:40 +02:00
Marcos Slomp
28d3a91980 more changes to allow for null context 2026-06-09 16:26:51 -07:00
Bartosz Taudul
3956616fc2 Fix repeated entries. 2026-06-10 01:14:22 +02:00
Marcos Slomp
0fbb2eaaa4 typo 2026-06-09 16:00:43 -07:00
Marcos Slomp
b27dab4584 remove "spontaneous" callback (better determinism) 2026-06-09 15:59:38 -07:00
Marcos Slomp
75bee5370f cosmetics 2026-06-09 15:58:24 -07:00
Marcos Slomp
e7499458e9 allow scoped instrumentation to no-op with null context 2026-06-09 15:58:06 -07:00
Bartosz Taudul
d34c45fa5a Use variable structure for random name generator. 2026-06-10 00:27:18 +02:00
Bartosz Taudul
8fe5a511c9 Expand name generator banks. 2026-06-10 00:15:55 +02:00
Bartosz Taudul
afdd2e2f81 Regenerate markdown manual. 2026-06-09 23:49:57 +02:00
Bartosz Taudul
3c1b1b2f80 Update manual. 2026-06-09 23:48:14 +02:00
Bartosz Taudul
992134f85e Fix regression with no tooltips on disabled items. 2026-06-09 23:31:59 +02:00
Bartosz Taudul
37bc986584 Add button for re-rolling annotation name. 2026-06-09 23:27:07 +02:00
Bartosz Taudul
feb4e7c989 Generate random names for new annotations. 2026-06-09 23:24:58 +02:00
Bartosz Taudul
4a8fe6f56e Add button to generate random trace description. 2026-06-09 23:22:21 +02:00
Bartosz Taudul
a960a25285 Add random name generator. 2026-06-09 23:22:21 +02:00
Marcos Slomp
958cb8d7f8 WGPU_PATH fix 2026-06-09 12:56:34 -07:00
Marcos Slomp
59f17794a5 fixing MemWrite casts 2026-06-09 09:06:48 -07:00
Marcos Slomp
3b2c7dbacb fixing webgpu lib linkage based on WGPU_PATH 2026-06-09 09:06:48 -07:00
Marcos Slomp
56ed480ed2 relocating webgpu example 2026-06-09 09:06:48 -07:00
Marcos Slomp
0572c86551 Wayland woes... 2026-06-09 09:06:48 -07:00
Marcos Slomp
6499e3383b fix Linux build 2026-06-09 09:06:48 -07:00
Marcos Slomp
8278ace0c1 build fix 2026-06-09 09:06:48 -07:00
Marcos Slomp
5981eca141 adding webgpu example/demo 2026-06-09 09:06:48 -07:00
Marcos Slomp
1b2856b885 GPU context name 2026-06-09 09:06:48 -07:00
Marcos Slomp
118f18cf4b updating docs 2026-06-09 09:06:48 -07:00
Marcos Slomp
bfbc1d3bee missing interface, and more debugging 2026-06-09 09:06:48 -07:00
Marcos Slomp
831779508f minor fixes/comments 2026-06-09 09:06:48 -07:00
Marcos Slomp
286309af3f refactoring calibration estimations 2026-06-09 09:06:47 -07:00
Marcos Slomp
3db70a2237 refactoring 2026-06-09 09:06:47 -07:00
Marcos Slomp
da952f3f38 more refactoring 2026-06-09 09:06:47 -07:00
Marcos Slomp
efba4685ef more cleanup and refactoring 2026-06-09 09:06:47 -07:00
Marcos Slomp
598984c45d refactoring initial calibration 2026-06-09 09:06:47 -07:00
Marcos Slomp
860011c604 calibration stability 2026-06-09 09:06:47 -07:00
Marcos Slomp
0cdcbfc75d refactoring query resolve 2026-06-09 09:06:47 -07:00
Marcos Slomp
e5d4be95df getting rid of spontaneous callbacks 2026-06-09 09:06:47 -07:00
Marcos Slomp
7b3863d93d redesign... 2026-06-09 09:06:47 -07:00
Marcos Slomp
de2a18d964 initial prototype for WebGPU back-end 2026-06-09 09:06:47 -07:00
Marcos Slomp
9588912aa9 Speeding-up GitHub build-bots (#1392)
* disabling LTO when building the profiler on macos via the github workflow

* diagnosing where in the linking stage it's getting stuck

* fowrward declarations

* compilation time report

* trying clang build analyzer...

* reducing number of parallel workers

* limiting parallel workers on windows/linux as well

* re-enabling LTO on macos

* reverting forward declaration header include (emscripten is failing with them).

* reverting act changes

* removing comments
2026-06-09 11:21:09 +02:00
Bartosz Taudul
7ee4380f64 Merge pull request #1395 from MaximeCoutantSonos/master
Add support for Shared Locks in the C API
2026-06-08 17:34:17 +02:00
Bartosz Taudul
01e639db97 Merge pull request #1336 from bmilanich/rocm-on-demand-fix
TracyRocprof: fix on-demand profiling crash and missing context name
2026-06-08 17:06:45 +02:00
Basil Milanich
030e699eb5 Move rocprof on-demand repro to tests/ and convert to CMake
Conform to the new repo layout (master moved repros under tests/, e.g.
tests/cuda/repro/graph). Relocate examples/RocprofOnDemandRepro to
tests/rocprof/repro/on_demand and replace the hand-written Makefile with
a CMake build mirroring the CUDA repro: builds the HIP reproducer, wires
it as a ctest target, and optionally builds the check_gpu_ctx_name
verification helper against the Tracy server library.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:47:22 -05:00
Basil Milanich
16cdf3d645 Merge remote-tracking branch 'origin/master' into rocm-on-demand-fix
# Conflicts:
#	public/client/TracyRocprof.cpp
2026-06-08 09:43:07 -05:00
MaximeCoutantSonos
2f143491eb Fixed typo in grammar 2026-06-08 16:07:06 +02:00
MaximeCoutantSonos
796050ac1e Added documentation for the shared lock C API 2026-06-08 15:52:54 +02:00
MaximeCoutantSonos
31dbfef97d Added C shared lock to C-API 2026-06-08 15:19:00 +02:00
Bartosz Taudul
19519bbeb0 Merge pull request #1394 from bruno-dasilva/bruno/opengl-drift-correction
fix: add opengl drift correction for gpu zones
2026-06-07 22:50:40 +02:00
Bruno Da Silva
fc4f52e61d add opengl drift correction option to meson.options/meson.build 2026-06-07 20:14:36 +00:00
Bruno Da Silva
e2ac8f7973 fix: add opengl drift correction for gpu zones 2026-06-07 00:23:14 +00:00
Bartosz Taudul
e5aa8eba51 Merge pull request #1387 from Lectem/wip/offline-res-for-any-toolchain
Offline resolution for any toolchain
2026-06-06 14:48:54 +02:00
Clément Grégoire
7437c41514 Escape provided addr2line tool path 2026-06-06 14:47:24 +02:00
Bartosz Taudul
f441a5070b Wrap achievements column contents into child windows. 2026-06-06 13:19:56 +02:00
Bartosz Taudul
00b6abd67b Move achievements text to markdown files. 2026-06-06 13:19:55 +02:00
Bartosz Taudul
e4e3d75eb8 Add PDF link to built-in manual viewer. 2026-06-06 10:56:25 +02:00
Bartosz Taudul
fc5318dcad Some more markdown user manual compatibility fixes. 2026-06-06 01:53:05 +02:00
Bartosz Taudul
661c664b75 Converte LaTeX math in markdown to plain text. 2026-06-06 01:44:27 +02:00
Bartosz Taudul
6dbebca666 Reset user manual view scroll position after changing section from toc. 2026-06-06 01:28:24 +02:00
Bartosz Taudul
73d78ad517 Fix tables in markdown manual. 2026-06-06 01:25:49 +02:00
Bartosz Taudul
e5371d7987 Icons description separator must start at newline.
Otherwise it will clash with wrong things, like middle-of-line table header
separators.
2026-06-06 01:25:08 +02:00
Bartosz Taudul
9806f35714 Increase spacing between admonition icon and header text. 2026-06-06 00:43:09 +02:00
Bartosz Taudul
d40289d594 Add support for markdown admonitions. 2026-06-05 23:28:09 +02:00
Bartosz Taudul
86fbe529ed Bump font awesome to 7.2. 2026-06-05 23:28:09 +02:00
Clément Grégoire
7cb98245ce Batch addr2line invocations by command-line length
The fixed batches of 1024 addresses could overflow the platform's command-line limit (`La ligne de commande est trop longue.` from cmd.exe on Windows, whose limit is ~8191 characters). Build each batch by appending addresses until a length budget is reached instead. A single conservative budget of 8000 stays under the smallest limit on every platform, and keeps batches in the same ballpark as before (several hundred addresses per invocation).
2026-06-05 19:10:19 +02:00
Clément Grégoire
55d5436fb9 Add option to reset callstack frame symbols to the unresolved state
The new `-R` option of tracy-update sets every callstack frame back to `[unresolved]` / `[unknown]`. Since failed lookups leave frames untouched and the image-relative offset in `symAddr` survives patching, this makes it possible to chain several resolution passes over the same capture, each with different `-p` path substitutions (e.g. one pass per symbol directory).
2026-06-05 19:03:35 +02:00
Clément Grégoire
2b11785b05 Allow offline symbol resolution with any addr2line-compatible tool
The addr2line backend of tracy-update now builds on every platform, including Windows, and can be pointed at any addr2line-compatible executable:

- `-a`: path to a custom symbol resolution tool (e.g. `llvm-addr2line` or a cross-compilation toolchain's `addr2line`). Works on all platforms and takes precedence over the platform default (DbgHelp on Windows, the `addr2line` found in `PATH` elsewhere). Path-like values are validated up front so a wrong path fails with an actionable message instead of a cryptic, localized shell error.
- `-A`: extra arguments passed verbatim to the tool, e.g. `--relative-address` so `llvm-addr2line`/`llvm-symbolizer` accept the image-relative offsets Tracy records for images with a non-zero preferred base (PE, Mach-O).
- `-v`: verbose output while patching symbols.
2026-06-05 19:03:35 +02:00
Basil Milanich
ebd3d9c3e6 Also defer GpuContextName for on-demand profiling
Without this, a late-connecting client receives the deferred
GpuNewContext but not the GpuContextName, so the GPU context appears
unnamed in the profiler.

Add check_gpu_ctx_name tool to verify context names in captured traces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:27:47 -05:00
Basil Milanich
bc8d8f5302 Add repro case for rocprofiler on-demand crash
Minimal HIP program that demonstrates the assertion failure in
tracy-capture when connecting to a TRACY_ON_DEMAND + TRACY_ROCPROF
application. See examples/RocprofOnDemandRepro/README.md for details.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:02:36 -05:00
Basil Milanich
b049746853 Fix on-demand profiling support for rocprofiler backend
Two issues prevented the rocprofiler GPU backend from working with
TRACY_ON_DEMAND:

1. GpuNewContext not deferred: When a Tracy client connects late (on-demand
   mode), it never receives the GPU context creation message because the
   GpuNewContext queue item was not buffered via DeferItem. This caused an
   assertion failure (ctx == nullptr) in the capture/profiler when
   processing GPU zone events. Add the same DeferItem pattern used by the
   CUDA backend.

2. Kernel symbols dropped before init: The data->init guard at the top of
   tool_callback_tracing_callback() blocked kernel symbol registrations
   (CODE_OBJECT_DEVICE_KERNEL_SYMBOL_REGISTER) which happen at HIP init
   time, before any Tracy client connects. Move the init guard after the
   code_object block so symbols are always recorded, while dispatch and
   memory-copy events are still gated on initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:56:31 -05:00
277 changed files with 17215 additions and 4365 deletions

View File

@@ -24,8 +24,8 @@ runs:
cmake --build tests/tracy/build --parallel
cmake -E rm -rf tests/tracy/build
# same with TRACY_DELAYED_INIT and TRACY_MANUAL_LIFETIME
cmake -B tests/tracy/build -S tests/tracy -DCMAKE_BUILD_TYPE=Release -DTRACY_DELAYED_INIT=ON -DTRACY_MANUAL_LIFETIME=ON ${{ inputs.extra_cmake_flags }}
# same with TRACY_MANUAL_LIFETIME
cmake -B tests/tracy/build -S tests/tracy -DCMAKE_BUILD_TYPE=Release -DTRACY_MANUAL_LIFETIME=ON ${{ inputs.extra_cmake_flags }}
cmake --build tests/tracy/build --parallel
cmake -E rm -rf tests/tracy/build

49
.github/workflows/android.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: android
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: android-${{ github.ref }}
cancel-in-progress: true
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
jobs:
build-android:
runs-on: ubuntu-latest
container: archlinux:base-devel
steps:
- name: Install dependencies
run: pacman -Syu --noconfirm && pacman -S --noconfirm --needed cmake git ninja nodejs unzip
- name: Setup NDK
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: r29
- run: echo "ANDROID_NDK_ROOT=${{ steps.setup-ndk.outputs.ndk-path }}" >> $GITHUB_ENV
- name: Trust git repo
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4
- name: Cache CPM packages
uses: actions/cache@v4
with:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Library v7
run: |
cmake -B build-v7 -DCMAKE_BUILD_TYPE=Release -DTRACY_ENABLE=ON -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake -DANDROID_ABI=armeabi-v7a
cmake --build build-v7
- name: Library v8
run: |
cmake -B build-v8 -DCMAKE_BUILD_TYPE=Release -DTRACY_ENABLE=ON -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a
cmake --build build-v8

55
.github/workflows/appimage.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: appimage
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
workflow_call:
permissions:
contents: read
concurrency:
group: appimage-${{ github.ref }}
cancel-in-progress: true
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
jobs:
build-appimage:
runs-on: ubuntu-latest
container: ubuntu:24.04
steps:
- name: Install dependencies
run: |
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential cmake ninja-build meson pkg-config git ca-certificates \
curl file desktop-file-utils nodejs patchelf \
libegl-dev libxkbcommon-dev libdbus-1-dev libssl-dev \
libffi-dev libexpat1-dev
- name: Trust git repo
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4
- name: Cache CPM packages
uses: actions/cache@v4
with:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Cache wayland
uses: actions/cache@v4
with:
path: profiler/build-appimage/wayland
key: ${{ runner.os }}-wayland-${{ hashFiles('profiler/appimage/build.sh') }}
- name: Build AppImage
run: profiler/appimage/build.sh
env:
GIT_REV: ${{ github.sha }}
- uses: actions/upload-artifact@v4
with:
name: appimage
path: profiler/build-appimage/tracy-profiler-x86_64.AppImage

View File

@@ -7,6 +7,13 @@ on:
branches: [ master ]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: emscripten-${{ github.ref }}
cancel-in-progress: true
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
@@ -20,7 +27,7 @@ jobs:
- name: Setup emscripten
uses: emscripten-core/setup-emsdk@v16
with:
version: 4.0.10
version: 5.0.7
- name: Trust git repo
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4

View File

@@ -8,6 +8,13 @@ on:
workflow_dispatch:
workflow_call:
permissions:
contents: read
concurrency:
group: latex-${{ github.ref }}
cancel-in-progress: true
jobs:
build-manual:

74
.github/workflows/linux-cli.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
name: linux-cli
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
workflow_call:
permissions:
contents: read
concurrency:
group: linux-cli-${{ github.ref }}
cancel-in-progress: true
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
jobs:
build-linux-cli:
runs-on: ubuntu-latest
container: ubuntu:24.04
steps:
- name: Install dependencies
run: |
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential cmake git ca-certificates curl nodejs
- name: Trust git repo
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4
- name: Cache CPM packages
uses: actions/cache@v4
with:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Build update
run: |
cmake -B update/build -S update -DCMAKE_BUILD_TYPE=Release -DNO_ISA_EXTENSIONS=ON -DGIT_REV=${{ github.sha }}
cmake --build update/build --parallel
- name: Build capture
run: |
cmake -B capture/build -S capture -DCMAKE_BUILD_TYPE=Release -DNO_ISA_EXTENSIONS=ON -DGIT_REV=${{ github.sha }}
cmake --build capture/build --parallel
- name: Build csvexport
run: |
cmake -B csvexport/build -S csvexport -DCMAKE_BUILD_TYPE=Release -DNO_ISA_EXTENSIONS=ON -DGIT_REV=${{ github.sha }}
cmake --build csvexport/build --parallel
- name: Build import
run: |
cmake -B import/build -S import -DCMAKE_BUILD_TYPE=Release -DNO_ISA_EXTENSIONS=ON -DGIT_REV=${{ github.sha }}
cmake --build import/build --parallel
- name: Build merge
run: |
cmake -B merge/build -S merge -DCMAKE_BUILD_TYPE=Release -DNO_ISA_EXTENSIONS=ON -DGIT_REV=${{ github.sha }}
cmake --build merge/build --parallel
- name: Stage release files
run: |
mkdir -p linux-cli
cp update/build/tracy-update linux-cli/
cp capture/build/tracy-capture linux-cli/
cp capture/build/tracy-capture-daemon linux-cli/
cp csvexport/build/tracy-csvexport linux-cli/
cp import/build/tracy-import-chrome linux-cli/
cp import/build/tracy-import-fuchsia linux-cli/
cp merge/build/tracy-merge linux-cli/
strip linux-cli/tracy-*
- uses: actions/upload-artifact@v4
with:
name: linux-cli
path: linux-cli

View File

@@ -7,6 +7,13 @@ on:
branches: [ master ]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: linux-${{ github.ref }}
cancel-in-progress: true
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
@@ -16,7 +23,7 @@ jobs:
container: archlinux:base-devel
steps:
- name: Install dependencies
run: pacman -Syu --noconfirm && pacman -S --noconfirm --needed freetype2 debuginfod wayland dbus libxkbcommon libglvnd meson cmake git wayland-protocols nodejs lua
run: pacman -Syu --noconfirm && pacman -S --noconfirm --needed freetype2 debuginfod wayland dbus libxkbcommon libglvnd meson cmake git wayland-protocols nodejs lua clang sdl3_image python-jinja
- name: Trust git repo
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4
@@ -26,39 +33,49 @@ jobs:
path: ${{ env.CPM_SOURCE_CACHE }}
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Determine build parallelism
run: |
if [ "${ACT:-}" != "true" ] && [ "${FORGEJO_ACTIONS:-}" != "true" ]; then
echo "CMAKE_PARALLEL=--parallel 2" >> "$GITHUB_ENV"
else
echo "CMAKE_PARALLEL=--parallel" >> "$GITHUB_ENV"
fi
- name: Dyna example / clang
run: |
CC=clang CXX=clang++ cmake -B examples/dyna/build-clang -S examples/dyna -DCMAKE_BUILD_TYPE=Release
cmake --build examples/dyna/build-clang $CMAKE_PARALLEL
- name: Dyna example / gcc
run: |
CC=gcc CXX=g++ cmake -B examples/dyna/build-gcc -S examples/dyna -DCMAKE_BUILD_TYPE=Release
cmake --build examples/dyna/build-gcc $CMAKE_PARALLEL
- name: Profiler GUI
run: |
cmake -B profiler/build -S profiler -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
if [ "${ACT:-}" != "true" ] && [ "${FORGEJO_ACTIONS:-}" != "true" ]; then
cmake --build profiler/build
else
cmake --build profiler/build --parallel
fi
cmake --build profiler/build $CMAKE_PARALLEL
- name: Update utility
run: |
cmake -B update/build -S update -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build update/build --parallel
cmake --build update/build $CMAKE_PARALLEL
- name: Capture utility
run: |
cmake -B capture/build -S capture -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build capture/build --parallel
cmake --build capture/build $CMAKE_PARALLEL
- name: Csvexport utility
run: |
cmake -B csvexport/build -S csvexport -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build csvexport/build --parallel
cmake --build csvexport/build $CMAKE_PARALLEL
- name: Import utilities
run: |
cmake -B import/build -S import -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build import/build --parallel
cmake --build import/build $CMAKE_PARALLEL
- name: Merge utility
run: |
cmake -B merge/build -S merge -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build merge/build --parallel
cmake --build merge/build $CMAKE_PARALLEL
- name: Library (cmake)
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Release -DTRACY_ENABLE=ON
cmake --build build
cmake --install build
cmake --build build $CMAKE_PARALLEL
env:
CMAKE_INSTALL_PREFIX: ${{ github.workspace }}/bin
- name: Library (meson)
@@ -74,6 +91,7 @@ jobs:
cp profiler/build/tracy-profiler bin
cp update/build/tracy-update bin
cp capture/build/tracy-capture bin
cp capture/build/tracy-capture-daemon bin
cp csvexport/build/tracy-csvexport bin
cp import/build/tracy-import-chrome bin
cp import/build/tracy-import-fuchsia bin

View File

@@ -6,13 +6,21 @@ on:
pull_request:
branches: [ master ]
workflow_dispatch:
workflow_call:
permissions:
contents: read
concurrency:
group: macos-${{ github.ref }}
cancel-in-progress: true
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
jobs:
build-macos:
runs-on: macos-15
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Cache CPM packages
@@ -22,13 +30,17 @@ jobs:
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- name: Install dependencies
run: brew install pkg-config glfw meson
run: brew install pkg-config meson
- name: Trust git repo
run: git config --global --add safe.directory '*'
- name: Build library
run: meson setup -Dtracy_enable=true build && meson compile -C build
- name: Test application
uses: ./.github/actions/test-tracy
- name: Build profiler
run: |
cmake -B profiler/build -S profiler -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build profiler/build --parallel --config Release
cmake -B profiler/build -S profiler -DCMAKE_BUILD_TYPE=Release -DBUNDLE=ON -DGIT_REV=${{ github.sha }}
cmake --build profiler/build --parallel 2 --config Release
- name: Build update
run: |
cmake -B update/build -S update -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
@@ -49,20 +61,19 @@ jobs:
run: |
cmake -B merge/build -S merge -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build merge/build --parallel --config Release
- name: Build library
run: meson setup -Dprefix=$GITHUB_WORKSPACE/bin/lib -Dtracy_enable=true build && meson compile -C build && meson install -C build
- name: Test application
uses: ./.github/actions/test-tracy
- name: Package artifacts
run: |
mkdir -p bin
cp profiler/build/tracy-profiler bin
cp -r profiler/build/tracy-profiler.app bin
cp update/build/tracy-update bin
cp capture/build/tracy-capture bin
cp capture/build/tracy-capture-daemon bin
cp csvexport/build/tracy-csvexport bin
cp import/build/tracy-import-chrome bin
cp import/build/tracy-import-fuchsia bin
cp merge/build/tracy-merge bin
strip bin/tracy-profiler.app/Contents/MacOS/tracy-profiler
strip bin/tracy-update bin/tracy-capture bin/tracy-capture-daemon bin/tracy-csvexport bin/tracy-import-chrome bin/tracy-import-fuchsia bin/tracy-merge
- uses: actions/upload-artifact@v4
with:
name: macos

View File

@@ -7,6 +7,13 @@ on:
branches: [ master ]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: mingw-${{ github.ref }}
cancel-in-progress: true
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache

View File

@@ -4,26 +4,62 @@ on:
release:
types: [published]
permissions:
contents: read
concurrency:
group: release-${{ github.event.release.tag_name }}
cancel-in-progress: false
jobs:
release-build-windows:
uses: ./.github/workflows/windows.yml
release-build-macos:
uses: ./.github/workflows/macos.yml
release-build-manual:
uses: ./.github/workflows/latex.yml
release-build-appimage:
uses: ./.github/workflows/appimage.yml
release-build-linux-cli:
uses: ./.github/workflows/linux-cli.yml
attach-to-release:
needs: [release-build-windows, release-build-manual]
needs: [release-build-windows, release-build-macos, release-build-manual, release-build-appimage, release-build-linux-cli]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
- name: Create versioned zip
- name: Create versioned Windows zip
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION_NO_V="${VERSION#v}"
cd windows
zip -r ../windows-$VERSION_NO_V.zip .
- name: Create versioned macOS zip
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION_NO_V="${VERSION#v}"
cd macos
chmod +x tracy-* tracy-profiler.app/Contents/MacOS/*
zip -r ../macos-$VERSION_NO_V.zip .
- name: Create versioned Linux zip
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION_NO_V="${VERSION#v}"
mkdir linux
cp appimage/* linux/
cp linux-cli/* linux/
cd linux
zip -r ../linux-$VERSION_NO_V.zip .
- uses: softprops/action-gh-release@v2
with:
files: |
windows-*.zip
macos-*.zip
linux-*.zip
manual/tracy.pdf

View File

@@ -8,6 +8,13 @@ on:
workflow_dispatch:
workflow_call:
permissions:
contents: read
concurrency:
group: windows-${{ github.ref }}
cancel-in-progress: true
env:
CPM_SOURCE_CACHE: ${{ github.workspace }}/cpm-cache
@@ -23,16 +30,14 @@ jobs:
key: ${{ runner.os }}-cpm-${{ hashFiles('**/vendor.cmake', '**/CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-cpm-
- uses: microsoft/setup-msbuild@v2
- uses: actions/setup-python@v2
with:
python-version: '3.x'
- run: pip install meson ninja
- name: Trust git repo
run: git config --global --add safe.directory '*'
- name: Test application
uses: ./.github/actions/test-tracy
- name: Build profiler
run: |
cmake -B profiler/build -S profiler -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build profiler/build --parallel --config Release
cmake --build profiler/build --parallel 2 --config Release
- name: Build update
run: |
cmake -B update/build -S update -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
@@ -53,14 +58,13 @@ jobs:
run: |
cmake -B merge/build -S merge -DCMAKE_BUILD_TYPE=Release -DGIT_REV=${{ github.sha }}
cmake --build merge/build --parallel --config Release
- name: Test application
uses: ./.github/actions/test-tracy
- name: Package artifacts
run: |
mkdir bin
copy profiler\build\Release\tracy-profiler.exe bin
copy update\build\Release\tracy-update.exe bin
copy capture\build\Release\tracy-capture.exe bin
copy capture\build\Release\tracy-capture-daemon.exe bin
copy csvexport\build\Release\tracy-csvexport.exe bin
copy import\build\Release\tracy-import-chrome.exe bin
copy import\build\Release\tracy-import-fuchsia.exe bin

13
.gitignore vendored
View File

@@ -1,16 +1,12 @@
.vs
_build
_compiler
tools/*
*.d
*.o
*.so
*.swp
*.obj
imgui.ini
test/tracy_test
test/tracy_test.exe
*/build/unix/*-*
manual/t*.aux
manual/t*.log
manual/t*.out
@@ -21,19 +17,16 @@ manual/t*.bbl
manual/t*.blg
manual/t*.fdb_latexmk
manual/t*.fls
profiler/build/win32/packages
profiler/build/win32/Tracy.aps
profiler/build-appimage
.deps/
.dirstamp
/_*/**
/**/__pycache__/**
extra/vswhere.exe
extra/tracy-build
.cache
.uv-cache/
.venv/
compile_commands.json
profiler/build/wasm/Tracy-release.*
profiler/build/wasm/Tracy-debug.*
profiler/build/wasm/embed.tracy
examples/ToyPathTracer/Windows/TestCpu
examples/ToyPathTracer/Windows/x64
*.user

View File

@@ -0,0 +1,340 @@
---
name: tracy-gui-verify
description: Build, serve, and drive the Tracy Profiler's emscripten (web) GUI in a headless browser with verified mouse/keyboard semantics. Use when developing or testing the profiler GUI. No X11/Wayland/DISPLAY required — the browser is the display server.
---
# Test the Tracy web GUI in a browser
Build the emscripten profiler GUI into an instrumented temporary workspace,
serve it, and drive it with the browser tool. The harness exports
(`debug_snapshot`/`debug_clear`) expose the app-side mouse/ID state — they
are the ground truth for targeting and verification, not screenshots.
Applies to this repo's emscripten build with ImGui 1.92.9b-docking and
Emscripten 5.0.7. Constants marked M are workstation-dependent — re-verify
them. Everything else is stable; do not re-derive it.
## Setup
```sh
sh <this-skill-dir>/setup.sh # workspace=/tmp/tracy-protocol
# or: TRACY_WEB_WS=/elsewhere TRACY_WEB_REPO=<repo> sh setup.sh
```
`<this-skill-dir>` is the project-scoped `.omp/skills/tracy-gui-verify/`
under the active project root — not a fixed home directory. If multiple
copies exist (one per project that has used this skill), use the copy
inside the repo being tested; `TRACY_WEB_REPO` controls which repo is
copied into the workspace.
The script: fresh source copy (excludes `.git` and build dirs) → applies
`harness.patch` (adds `debug_snapshot`/`debug_clear` exports + event/frame
recording to the emscripten backend, in the COPY only) → configures → builds
Release with the emsdk at `$EMSDK_DIR` (default `~/emsdk`) and the CPM cache at
`~/.cache/cpm`.
**Pitfall:** if the workspace is deleted while a `tracy-web` server is running,
stop the hub process first — a stale server keeps the *deleted* cwd and silently
returns empty responses while the port still accepts connections.
**Pitfall:** a `tracy-web` that died in an earlier session leaves the daemon in
`failed` state: `stop` only reports the old failure, and `start` keeps returning
"Daemon tracy-web has unacknowledged completion notifications" while the port
stays free. Drain the daemon with `hub op=logs name=tracy-web` (the log also
shows the cause), then relaunch with `hub op=restart name=tracy-web` (retained
spec). If the port is NOT free, a stale process owns it — often one serving an
earlier (now-stale) build directory: the new `httpd.py` dies on the bind and
your requests silently hit the old directory. Check `ss -tlnp | grep 8000` and
kill the owner. After any start/restart, verify the server is actually
serving: `curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8000/index.html`
must return 200 — the daemon message is not proof of a server.
**After editing the GUI in the repo:** copy the changed files into
`$WS/src/` (same relative path), re-run `cmake --build $WS/build -j$(nproc)`,
reload the page. If a change touches `profiler/src/BackendEmscripten.cpp` or
`profiler/CMakeLists.txt`, the harness patch may no longer apply — re-apply it
manually (keep the `Dbg*` harness section, the `DbgPushEvent`/`DbgPushFrame`
calls in the mouse callbacks + `NewFrame`, and the two `debug_*` exports in
`-sEXPORTED_FUNCTIONS`).
## Serve + open + ready
Server (long-running → `hub`):
```
hub op=start name=tracy-web application=python3 args=["$WS/build/httpd.py"]
cwd=$WS/build ready={port:8000} persist=true
```
`httpd.py` sends the COOP/COEP headers required for pthreads/SharedArrayBuffer —
use it, not a bare http.server.
Browser:
```
browser open url=http://127.0.0.1:8000/index.html viewport={width:1600,height:900}
```
**Ready signal** (in a `run` cell; `run` code executes in Node scope — page JS
goes through `tab.evaluate`):
```js
let title = null;
for (let i = 0; i < 360; i++) {
title = await tab.evaluate(() => document.title);
if (/ - Tracy Profiler/.test(title)) break;
await new Promise(r => setTimeout(r, 250));
}
```
Title is `Tracy Profiler X.Y.Z` until the preloaded `embed.tracy` (DarkRL
capture) finishes loading, then `<trace> (embed.tracy) - Tracy Profiler X.Y.Z`
(`profiler/src/main.cpp` `SetWindowTitleCallback`). Wait ~1.5 s after, then
confirm the app loop lives: `snap().frames.at(-1).tick > 0`.
## Harness API (ground truth)
```js
const snap = () => tab.evaluate(() => JSON.parse(Module.ccall('debug_snapshot','string',[],[])));
const clear = () => tab.evaluate(() => Module.ccall('debug_clear','void',[],[]));
```
`snap()``{ dpr, innerW, innerH, bufW, bufH, rectL, rectT, rectW, rectH,
title, cursor, frames:[…64], events:[…64] }` (newest last).
- `frames[]`: one sample **per rAF tick, including idle ticks**. `tick` = main-loop
count (always advances), `f` = `ImGui::GetFrameCount()` (advances only when a
frame actually renders — the idle gate, §Idle), `mx/my` = app mouse position,
`md[3]` = mouse button states, `q` = pending input events, `wa` =
`tracy::s_wasActive` at tick start, `hi`/`ai` = ImGui `HoveredId`/`ActiveId`.
- `events[]`: raw callback data as the app received it. `type` 0=move 1=down
2=up 3=enter 4=leave 5=wheel; `tx/ty` = Emscripten int `targetX/Y`; `ax/ay` =
position passed to ImGui (`-1` for non-move); `t` = DOMHighResTimeStamp.
## Coordinate mapping
```
appX = floor( trunc(cssX - rectL) * dpr ) // device px, what ImGui hit-tests
cssX = appX / dpr // inverse for targeting
screenshot_px = cssX * dpr
```
Chain: DOM `clientX` → Emscripten `targetX = int(clientX - (rect.left|0))`
(`src/lib/libhtml5.js`, `EmscriptenMouseEvent.targetX` is an `int`) → Tracy
`AddMousePosEvent(targetX * dpr, …)` (`BackendEmscripten.cpp`) → ImGui `ImFloor`
(`imgui.cpp` `AddMousePosEvent`). Canvas fills the viewport, so `rectL/rectT`
are 0 (re-read anyway).
**M: `dpr` on this workstation's headless browser is 1.25** (canvas buffer
2000×1125 for a 1600×900 viewport; screenshots are captured at device px).
Never assume 1. App positions come in multiples of `dpr`; verify targeting
within ±1 px via `frames[].mx/my`.
## Click protocol
Click = move, press, refresh through the hold, release, verify — all
against rendered frames. The idle gate (§Idle) renders only 3 frames per
wake, and a hold with no mouse events at all refreshes nothing, so a
fixed-wait down/up from a cold idle can be swallowed (both events received,
zero frames rendered) or released after the wake budget expires. Issuing
mouse moves while the button is down keeps the budget alive: a same-coord
re-move is delivered (verified: it reaches the canvas handler, wakes the
app, and a same-coord hold click straddles cleanly) and is the safest
refresh — zero hover drift; a ±`dx` px wiggle works too, but must stay
inside the hit area:
```js
async function click(cssX, cssY, holdMs = 400) {
await page.mouse.move(cssX, cssY); // 1. ALWAYS move first
await new Promise(r => setTimeout(r, 150)); // 2. >= 1 rendered frame
const pre = await snap();
const pf = pre.frames.at(-1);
await clear();
await page.mouse.down(); // 3. press
let df = null, hiDuring = null;
const t0 = Date.now();
while (Date.now() - t0 < holdMs) {
await new Promise(r => setTimeout(r, 70));
await page.mouse.move(cssX, cssY); // 4. same-coord re-move refreshes the wake budget
const f = (await snap()).frames.at(-1);
if (f.md[0] === 1) { df ??= f.f; hiDuring = f.hi; }
}
await page.mouse.up(); // 5. release
let uf = null;
for (let i = 0; i < 40; i++) {
await new Promise(r => setTimeout(r, 40));
const f = (await snap()).frames.at(-1);
if (f.md[0] === 0) { uf = f.f; break; }
}
await new Promise(r => setTimeout(r, 400));
const s = await snap();
const d = s.dpr, ex = Math.floor(Math.trunc(cssX - s.rectL) * d), ey = Math.floor(Math.trunc(cssY - s.rectT) * d);
return { hoverID: pf.hi, hiDuring, appPos: [pf.mx, pf.my], expected: [ex, ey],
straddle: { df, uf },
ok: df !== null && uf !== null && uf > df && hiDuring === pf.hi &&
Math.abs(pf.mx - ex) <= 1 && Math.abs(pf.my - ey) <= 1 };
}
```
Rules:
1. **Move before every click.** The emscripten mousemove handler is the only
thing that sets the app mouse position; mousedown/mouseup only queue button
state. Fresh page position is invalid (`-FLT_MAX`) — a click without a prior
move activates nothing.
2. **Press and release must each render** in separate frames with the item
hovered (ImGui's `PressedOnClickRelease` — doc table at
`imgui_widgets.cpp:489-497`; input trickling,
`ConfigInputTrickleEventQueue`, defers a same-frame release). The
same-coord refresh is what guarantees rendered frames across the hold
from a cold idle.
3. **Verify every click:** `ok: true` (position landed, press and release each
rendered, hover held — `hiDuring === hoverID`) plus a screenshot diff.
Non-ImGui targets (timeline zone bars) have `hi = 0` by design — for those
verify by screenshot, not hover.
4. **Double-click trap:** two same-position clicks <300 ms apart = double-click
(`MouseDoubleClickTime = 0.3 s`) → zoom/thread-info actions fire. Wait
≥350 ms between repeated clicks at one spot.
5. **Drag:** move → down → (move + wait 70 ms)×n → up. **Wheel:**
`page.mouse.wheel({ deltaY: ±500 })` after positioning. **Keys:**
`page.keyboard.type(..., { delay: 50 })` after focusing the input by click;
`page.keyboard.press('Enter')`.
6. **Danger:** toolbar device x 1032 (css 826) = red power button (leftmost
toolbar icon, hover ID 3313137574), closes the trace view (back to "Get
started"). Never click it unless intended.
## Idle and power saving
`main.cpp DrawContents` keeps an `activeFrames = 3` budget refreshed on any
input event (`tracy::s_wasActive`), a connected client, view animation, or a
queued input; exhausted → `sleep 16 ms` with **no `ImGui::NewFrame` and no GL
draw** (canvas keeps the last frame). Expect ~97 % of ticks to skip rendering
while idle.
- Clicks from a cold idle land only via the click protocol's hold refresh
(§Click protocol) — a fixed-wait down/up can be swallowed by the idle gate
(both events received, zero frames rendered). **Before interacting, assert
`tick`/`f` is advancing**; a stalled main loop (initial trace parse, hidden
tab) renders nothing and drops everything. During the initial load the
cursor style is `wait`.
- `focusLostLimit` (reduce render rate on focus loss) is desktop-only; it does
not exist in the emscripten path.
- `canvas.style.cursor` is **not** a hover oracle: this ImGui build sets the
Hand cursor only in `TextLink`, not on buttons.
## Targeting
Screenshots never resolve targeting. Full-page shots arrive downscaled JPEG
(1024 px wide for a 2000-px device buffer; the factor varies), and
`page.screenshot({ clip })` returns a PNG at device scale — a W×H CSS clip
returns W·dpr × H·dpr px (CSS = clip offset + px/dpr). Icon buttons are
1525 device px wide, so visual estimates carry ±20 px error, enough to hit
the neighboring button or a gap. Target from the app's own hit-testing —
sweep the row and click the run's center:
```js
async function sweepRow(dpr, cssY, devFrom, devTo, step = 4) {
const ranges = []; let cur = null;
for (let dev = devFrom; dev <= devTo; dev += step) {
await page.mouse.move(dev / dpr, cssY);
await new Promise(r => setTimeout(r, 50));
const s = await snap();
const hi = s.frames[s.frames.length - 1].hi;
if (cur && hi === cur.id) cur.end = dev;
else { if (cur) ranges.push(cur); cur = { id: hi, start: dev, end: dev }; }
}
ranges.push(cur);
return ranges.filter(r => r.id !== 0 && r.end - r.start >= 4);
}
```
- Same non-zero `hi` across consecutive steps = one widget; `hi = 0` = gap.
Click the run's center: `click((r.start + r.end) / 2 / dpr, cssY)`.
- **Layout is state-dependent:** docked panels (Statistics, Flame, …) resize the
main window and shift the toolbar — **re-sweep after any state change.**
- **Close popups before sweeping:** while a popup is open, hovers outside the
popup can read `hi = 0` even over live widgets (verified with ZoomPopup over
the toolbar). A % click in ZoomPopup does not close the popup — click an
empty area first.
- Hover IDs are stable across sessions (label hashes) — the map below holds for
the default state (fresh load, no panels, 1600×900, dpr 1.25).
- Zoomed region shots (for correlating IDs with icons): `page.screenshot({clip})`
returns a Buffer — write it with Node `fs`, then `read` the file:
```js
const fs = require('fs');
const buf = await page.screenshot({ clip: { x: 0, y: 0, width: 720, height: 28 } });
fs.writeFileSync('/tmp/toolbar-zoom.png', buf);
```
### Toolbar map — default state (device px; css = device/1.25; row y css 15)
| device x | hover ID | function |
|---|---|---|
| 1032 | 3313137574 | ⏻ power (red, leftmost) — **closes the trace view (DANGER)** |
| 4468 | 608241489 | ⚙ Options gear — opens the Options window (clickable; do not confuse with the power button) |
| 81185 | 2246346107 | 💬 Messages panel toggle |
| 199259 | 3177774389 | 🔍 Find zone toggle (search input hover ID 3637961452, css ~x 100440, y ~90) |
| 271367 | 1419322276 | 📊 Statistics toggle |
| 383451 | 322727105 | 🔥 Flame graph toggle |
| 463551 | 37993378 | 💾 Memory graph toggle |
| 567667 | 2133895361 | ⚖ Compare toggle |
| 679735 | 1808024465 | ⌗ Info (trace information) toggle |
| 751775 | 2535445525 | 🛠 wrench → ToolsPopup (Playback, CPU data, Annotations, Limits, Wait stacks, Frame statistics) |
| 791811 | 3101356487 | 📖 book → User manual toggle |
| 827851 | 2335673581 | 🔍+ → ZoomPopup (50300 % user scale) |
| 864888 | 1263603982 | ◀ previous frame — focuses the timeline on the previous frame (view range shrinks to the frame's timespan) |
| ~8901010 | 0 | frame set name + count text ("Frames: N") — LMB jumps to a specified frame (custom hit-test, like timeline zones) |
| 10121036 | 2142681495 | ▶ next frame — focuses the timeline on the next frame (verified: view range 25.82 s → 203.38 ms) |
| 10521072 | 2961929287 | ▼ frame set selection (switch the active frame set) |
Button names and functions: the user manual, *Control menu* (`manual/tracy.md`). The
web build omits *Connection* (live capture only) and *Tracy Assist* (desktop only).
## User scale (DPI zoom)
The 🔍+ button (hover ID 2335673581) — *Display scale* in the user manual —
opens a ZoomPopup with 50300 % steps.
In the web build this scales the UI **layout only**: fonts,
`Style::ScaleAllSizes`, and window sizes (`main.cpp SetupDPIScale`,
`scale = devicePixelRatio × userScale`). The canvas buffer and the mouse→app
mapping stay at `devicePixelRatio` (`BackendEmscripten.cpp`) — hit regions
become physically larger in the same coordinate space, targeting precision is
unchanged, and the standard click protocol works as-is.
Use it when small buttons are hard to hit or details are hard to read in
screenshots. At 200 % every toolbar hit region is roughly 2× (verified: power
16→40 px, gear 25→50 px, Messages 105→190 px wide; row height 24→46 device
px). The layout **reflows completely** at any scale — re-sweep after changing
it and after setting it back. `userScale` is session-only unless Options →
"Save UI scale" is on; a fresh page load starts at 100 %.
## Limitations
- A *connected* live-streaming client cannot be tested in the web build (the
browser GUI cannot open a TCP server). The embedded-trace GUI is the
live-app proxy: real trace data, full GUI.
- `embed.tracy` (10 MB) is downloaded from share.nereid.pl at configure time —
network needed once per fresh workspace; the file then lives in `$WS/build`.
## References
Sources for the behavior the rules above encode — consult them when re-verifying
a constant or a rule fails:
- `profiler/src/BackendEmscripten.cpp` — emscripten input handlers, canvas/dpr
sizing, cursor mapping.
- `profiler/src/main.cpp:540-574` — idle gate (`activeFrames` budget).
- `profiler/src/profiler/TracyMouse.cpp` — per-frame click state cache.
- `profiler/CMakeLists.txt:334-353` — emscripten link options.
- ImGui 1.92.9b (vendored copy in the CPM cache, `~/.cache/cpm/imgui/`):
`imgui_widgets.cpp:483-545` (click semantics table), `imgui.cpp:11278-11403`
(event trickling), `imgui.cpp:1736` (trickle default on), `imgui.cpp:1992-2016`
(AddMousePosEvent floor/dedup).
- Emscripten 5.0.7: `src/lib/libhtml5.js` (`fillMouseEventData`,
`targetX = e.clientX - (rect.left|0)` into HEAP32), `system/include/emscripten/html5.h`
(`EmscriptenMouseEvent.targetX` is `int`; `EmscriptenWheelEvent` embeds a
`mouse` sub-struct).
- Upstream: imgui issue #4921 (new input event API — events spread over
multiple frames), PR #2525 (same-frame click/release), issue #1992 (rapid
down/up "soft clicks" dropped).

View File

@@ -0,0 +1,221 @@
diff -r a/profiler/CMakeLists.txt b/profiler/CMakeLists.txt
--- a/profiler/CMakeLists.txt
+++ b/profiler/CMakeLists.txt
@@ -340,7 +340,7 @@
-sMAXIMUM_MEMORY=4gb
-sSTACK_SIZE=1048576
-sPTHREAD_POOL_SIZE=8
- -sEXPORTED_FUNCTIONS=_main,_nativeOpenFile,_tracy_paste_clipboard
+ -sEXPORTED_FUNCTIONS=_main,_nativeOpenFile,_tracy_paste_clipboard,_debug_snapshot,_debug_clear
-sEXPORTED_RUNTIME_METHODS=ccall
-sENVIRONMENT=web,worker
--preload-file embed.tracy
diff -r a/profiler/src/BackendEmscripten.cpp b/profiler/src/BackendEmscripten.cpp
--- a/profiler/src/BackendEmscripten.cpp
+++ b/profiler/src/BackendEmscripten.cpp
@@ -1,6 +1,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <cstdarg>
+#include <algorithm>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
@@ -10,7 +12,146 @@
#include "Backend.hpp"
#include "RunQueue.hpp"
#include "profiler/TracyImGui.hpp"
+#include "imgui_internal.h"
+// ================= DEBUG HARNESS (temporary copy, not for upstream) =================
+struct DbgEvent
+{
+ int type; // 0=move 1=down 2=up 3=enter 4=leave 5=wheel
+ int button;
+ int targetX;
+ int targetY;
+ double appX; // what was passed to ImGui (targetX * dpr), -1 if n/a
+ double appY;
+ double t; // DOMHighResTimeStamp
+ int wheelX;
+ int wheelY;
+};
+struct DbgFrame
+{
+ double t;
+ int tick; // emscripten main-loop tick count (rAF)
+ int imguiFrame; // ImGui::GetFrameCount()
+ float mx, my; // io.MousePos
+ int md0, md1, md2;
+ int queue; // pending ImGui input events
+ int wasActive; // tracy::s_wasActive at tick start
+ unsigned hi; // ImGui hovered item ID
+ unsigned ai; // ImGui active item ID
+};
+static DbgEvent s_dbgEvents[512];
+static int s_dbgEventHead = 0, s_dbgEventN = 0;
+static DbgFrame s_dbgFrames[512];
+static int s_dbgFrameHead = 0, s_dbgFrameN = 0;
+static int s_dbgTick = 0;
+
+static void DbgPushEvent( int type, int button, int tx, int ty, double ax, double ay, double t, int wx = 0, int wy = 0 )
+{
+ auto& e = s_dbgEvents[s_dbgEventHead];
+ e.type = type; e.button = button; e.targetX = tx; e.targetY = ty;
+ e.appX = ax; e.appY = ay; e.t = t; e.wheelX = wx; e.wheelY = wy;
+ s_dbgEventHead = ( s_dbgEventHead + 1 ) % 512;
+ if ( s_dbgEventN < 512 ) s_dbgEventN++;
+}
+
+static void DbgPushFrame()
+{
+ auto& f = s_dbgFrames[s_dbgFrameHead];
+ auto& io = ImGui::GetIO();
+ f.t = EM_ASM_DOUBLE( { return performance.now(); } );
+ f.tick = s_dbgTick;
+ f.imguiFrame = ImGui::GetFrameCount();
+ f.mx = io.MousePos.x; f.my = io.MousePos.y;
+ f.md0 = io.MouseDown[0]; f.md1 = io.MouseDown[1]; f.md2 = io.MouseDown[2];
+ auto ctx = ImGui::GetCurrentContext();
+ f.queue = ctx ? int( ctx->InputEventsQueue.Size ) : -1;
+ f.wasActive = tracy::s_wasActive;
+ f.hi = ctx ? ctx->HoveredId : 0;
+ f.ai = ctx ? ctx->ActiveId : 0;
+ s_dbgFrameHead = ( s_dbgFrameHead + 1 ) % 512;
+ if ( s_dbgFrameN < 512 ) s_dbgFrameN++;
+}
+
+extern "C" EMSCRIPTEN_KEEPALIVE void debug_clear()
+{
+ s_dbgEventHead = s_dbgEventN = s_dbgFrameHead = s_dbgFrameN = 0;
+}
+
+// Escape a C string for embedding in a JSON string literal (", \, control chars).
+static void JsonEscape( char* out, size_t outsz, const char* in )
+{
+ size_t o = 0;
+ for( const unsigned char* p = ( const unsigned char* )in; *p; p++ )
+ {
+ if ( *p == '"' || *p == '\\' )
+ {
+ if ( o + 2 >= outsz ) break;
+ out[o++] = '\\';
+ out[o++] = char( *p );
+ }
+ else if ( *p < 0x20 )
+ {
+ if ( o + 6 >= outsz ) break;
+ o += size_t( snprintf( out + o, outsz - o, "\\u%04x", *p ) );
+ }
+ else
+ {
+ if ( o + 1 >= outsz ) break;
+ out[o++] = char( *p );
+ }
+ }
+ out[o] = 0;
+}
+
+extern "C" EMSCRIPTEN_KEEPALIVE const char* debug_snapshot()
+{
+ static char buf[512 * 1024];
+ size_t off = 0;
+ auto put = [ & ]( const char* fmt, ... )
+ {
+ if ( off + 1 >= sizeof( buf ) ) return;
+ va_list args;
+ va_start( args, fmt );
+ int n = vsnprintf( buf + off, sizeof( buf ) - off - 1, fmt, args );
+ va_end( args );
+ if ( n > 0 ) off = std::min( off + size_t( n ), sizeof( buf ) - 1 );
+ };
+ const auto dpr = EM_ASM_DOUBLE( { return window.devicePixelRatio; } );
+ const auto innerW = EM_ASM_INT( { return window.innerWidth; } );
+ const auto innerH = EM_ASM_INT( { return window.innerHeight; } );
+ const auto bufW = EM_ASM_INT( { return document.getElementById( 'canvas' ).width; } );
+ const auto bufH = EM_ASM_INT( { return document.getElementById( 'canvas' ).height; } );
+ const auto rectL = EM_ASM_DOUBLE( { var r = document.getElementById( 'canvas' ).getBoundingClientRect(); return r.left; } );
+ const auto rectT = EM_ASM_DOUBLE( { var r = document.getElementById( 'canvas' ).getBoundingClientRect(); return r.top; } );
+ const auto rectW = EM_ASM_DOUBLE( { var r = document.getElementById( 'canvas' ).getBoundingClientRect(); return r.width; } );
+ const auto rectH = EM_ASM_DOUBLE( { var r = document.getElementById( 'canvas' ).getBoundingClientRect(); return r.height; } );
+ char title[256];
+ EM_ASM( { stringToUTF8( document.title, $0, 256 ); }, title );
+ char cursor[64];
+ EM_ASM( { stringToUTF8( document.getElementById( 'canvas' ).style.cursor || "", $0, 64 ); }, cursor );
+ char title_e[1536];
+ char cursor_e[384];
+ JsonEscape( title_e, sizeof( title_e ), title );
+ JsonEscape( cursor_e, sizeof( cursor_e ), cursor );
+ put( "{\"dpr\":%.6f,\"innerW\":%d,\"innerH\":%d,\"bufW\":%d,\"bufH\":%d,\"rectL\":%.3f,\"rectT\":%.3f,\"rectW\":%.3f,\"rectH\":%.3f,\"title\":\"%s\",\"cursor\":\"%s\",\"frames\":[", dpr, innerW, innerH, bufW, bufH, rectL, rectT, rectW, rectH, title_e, cursor_e );
+ const int fn = std::min( s_dbgFrameN, 64 );
+ for ( int i = 0; i < fn; i++ )
+ {
+ auto& f = s_dbgFrames[ ( s_dbgFrameHead - fn + i + 512 ) % 512 ];
+ put( "%s{\"t\":%.3f,\"tick\":%d,\"f\":%d,\"mx\":%.1f,\"my\":%.1f,\"md\":[%d,%d,%d],\"q\":%d,\"wa\":%d,\"hi\":%u,\"ai\":%u}", i ? "," : "", f.t, f.tick, f.imguiFrame, f.mx, f.my, f.md0, f.md1, f.md2, f.queue, f.wasActive, f.hi, f.ai );
+ }
+ put( "],\"events\":[");
+ const int en = std::min( s_dbgEventN, 64 );
+ for ( int i = 0; i < en; i++ )
+ {
+ auto& e = s_dbgEvents[ ( s_dbgEventHead - en + i + 512 ) % 512 ];
+ put( "%s{\"type\":%d,\"btn\":%d,\"tx\":%d,\"ty\":%d,\"ax\":%.3f,\"ay\":%.3f,\"t\":%.3f,\"wx\":%d,\"wy\":%d}", i ? "," : "", e.type, e.button, e.targetX, e.targetY, e.appX, e.appY, e.t, e.wheelX, e.wheelY );
+ }
+ put( "]}");
+ buf[off] = 0;
+ return buf;
+}
+
static std::function<void()> s_redraw;
static std::function<void(float)> s_scaleChanged;
static std::function<int(void)> s_isBusy;
@@ -223,32 +364,38 @@
emscripten_set_mousedown_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
ImGui::GetIO().AddMouseButtonEvent( e->button == 0 ? 0 : 3 - e->button, true );
tracy::s_wasActive = true;
+ DbgPushEvent( 1, e->button, e->targetX, e->targetY, -1, -1, e->timestamp );
return EM_TRUE;
} );
emscripten_set_mouseup_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
ImGui::GetIO().AddMouseButtonEvent( e->button == 0 ? 0 : 3 - e->button, false );
tracy::s_wasActive = true;
+ DbgPushEvent( 2, e->button, e->targetX, e->targetY, -1, -1, e->timestamp );
return EM_TRUE;
} );
emscripten_set_mousemove_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
const auto scale = EM_ASM_DOUBLE( { return window.devicePixelRatio; } );
ImGui::GetIO().AddMousePosEvent( e->targetX * scale, e->targetY * scale );
tracy::s_wasActive = true;
+ DbgPushEvent( 0, -1, e->targetX, e->targetY, e->targetX * scale, e->targetY * scale, e->timestamp );
return EM_TRUE;
} );
- emscripten_set_mouseleave_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent*, void* ) -> EM_BOOL {
+ emscripten_set_mouseleave_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
ImGui::GetIO().AddFocusEvent( false );
tracy::s_wasActive = true;
+ DbgPushEvent( 4, -1, e->targetX, e->targetY, -1, -1, e->timestamp );
return EM_TRUE;
} );
- emscripten_set_mouseenter_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent*, void* ) -> EM_BOOL {
+ emscripten_set_mouseenter_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenMouseEvent* e, void* ) -> EM_BOOL {
ImGui::GetIO().AddFocusEvent( true );
tracy::s_wasActive = true;
+ DbgPushEvent( 3, -1, e->targetX, e->targetY, -1, -1, e->timestamp );
return EM_TRUE;
} );
emscripten_set_wheel_callback( "#canvas", nullptr, EM_TRUE, []( int, const EmscriptenWheelEvent* e, void* ) -> EM_BOOL {
ImGui::GetIO().AddMouseWheelEvent( e->deltaX * -0.05, e->deltaY * -0.05 );
tracy::s_wasActive = true;
+ DbgPushEvent( 5, -1, e->mouse.targetX, e->mouse.targetY, -1, -1, e->mouse.timestamp, int( e->deltaX ), int( e->deltaY ) );
return EM_TRUE;
} );
emscripten_set_keydown_callback( EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_TRUE, [] ( int, const EmscriptenKeyboardEvent* e, void* ) -> EM_BOOL {
@@ -298,6 +445,8 @@
void Backend::NewFrame( int& w, int& h )
{
+ s_dbgTick++;
+ DbgPushFrame();
const auto scale = GetDpiScale();
if( scale != s_prevScale )
{

View File

@@ -0,0 +1,102 @@
#!/bin/sh
# setup.sh - build the instrumented Tracy web GUI workspace (see SKILL.md).
#
# Usage:
# sh setup.sh
# TRACY_WEB_WS=/custom/ws TRACY_WEB_REPO=<repo> sh setup.sh
#
# Idempotent: reuses an existing source copy and build dir, skips
# already-applied harness patch hunks, rebuilds.
set -e
WS="${TRACY_WEB_WS:-/tmp/tracy-protocol}"
# The skill is project-scoped: <repo>/.omp/skills/tracy-gui-verify/
SKILL_DIR=$(cd "$(dirname "$0")" && pwd)
if [ -n "$TRACY_WEB_REPO" ]; then
REPO="$TRACY_WEB_REPO"
else
REPO=$(cd "$SKILL_DIR/../../.." 2>/dev/null && pwd) || REPO=""
fi
EMSDK="${EMSDK_DIR:-$HOME/emsdk}"
[ -x "$EMSDK/emsdk" ] || { echo "error: emsdk not found at $EMSDK (set EMSDK_DIR)" >&2; exit 1; }
[ -f "$REPO/profiler/CMakeLists.txt" ] || { echo "error: not a Tracy repo: '$REPO' (set TRACY_WEB_REPO)" >&2; exit 1; }
. "$EMSDK/emsdk_env.sh" >/dev/null 2>&1 || true
command -v emcc >/dev/null || { echo "error: emcc not on PATH after emsdk_env.sh" >&2; exit 1; }
if [ ! -f "$WS/src/profiler/CMakeLists.txt" ]; then
echo "[setup] fresh source copy -> $WS/src (from $REPO)"
mkdir -p "$WS/src"
# -C applies to both archive creation and extraction; exclude all build dirs
tar -C "$REPO" \
--exclude=.git \
--exclude='*/build*' \
-cf - . | tar -xf - -C "$WS/src"
else
echo "[setup] reusing source copy in $WS/src (copy changed repo files into it first)"
fi
# The harness is fully in place iff each patch hunk's signature line is present.
# patch --forward skips a whole file once its first hunk is detected as already
# applied, so a failed later hunk is invisible in patch's output and exit code —
# verify the content instead of trusting either.
harness_complete()
{
grep -q '_debug_snapshot' "$WS/src/profiler/CMakeLists.txt" \
&& grep -qF 'EMSCRIPTEN_KEEPALIVE const char* debug_snapshot' "$WS/src/profiler/src/BackendEmscripten.cpp" \
&& grep -qF 'DbgPushEvent( 1,' "$WS/src/profiler/src/BackendEmscripten.cpp" \
&& grep -qF 's_dbgTick++;' "$WS/src/profiler/src/BackendEmscripten.cpp"
}
echo "[setup] applying instrumentation harness"
if harness_complete; then
echo "[setup] harness already applied"
rm -f "$WS/src/profiler/CMakeLists.txt.rej" "$WS/src/profiler/src/BackendEmscripten.cpp.rej"
else
rm -f "$WS/src/profiler/CMakeLists.txt.rej" "$WS/src/profiler/src/BackendEmscripten.cpp.rej"
patch -p1 -d "$WS/src" --forward --no-backup-if-mismatch < "$SKILL_DIR/harness.patch" \
|| {
if harness_complete; then
# mixed run: some hunks already present (skipped), the rest applied
rm -f "$WS/src/profiler/CMakeLists.txt.rej" "$WS/src/profiler/src/BackendEmscripten.cpp.rej"
else
rej=$(ls "$WS/src/profiler/CMakeLists.txt.rej" "$WS/src/profiler/src/BackendEmscripten.cpp.rej" 2>/dev/null || true)
if [ -n "$rej" ]; then
kept="The unapplied hunks are kept in:
$(printf '%s\n' $rej | sed 's/^/ /')"
else
kept="No .rej files were produced (fatal patch error); the patch output above is the only evidence."
fi
cat >&2 <<EOF
error: harness patch did not fully apply — $WS/src has drifted from the repo.
$kept
Do not build from this workspace. Either re-apply the harness by hand
(SKILL.md, "After editing the GUI in the repo"), or start fresh:
rm -rf $WS && sh <this-skill-dir>/setup.sh
EOF
exit 1
fi
}
fi
if [ ! -f "$WS/build/CMakeCache.txt" ]; then
echo "[setup] configuring"
cmake -S "$WS/src/profiler" -B "$WS/build" \
-DCMAKE_TOOLCHAIN_FILE="$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DCPM_SOURCE_CACHE="$HOME/.cache/cpm"
fi
echo "[setup] building (first full build ~3-5 min, incremental ~30 s)"
cmake --build "$WS/build" -j "$(nproc)"
cat <<EOF
[setup] done.
workspace : $WS
repo : $REPO
emcc : $(emcc --version | sed -n 1p)
serve : hub op=start name=tracy-web application=python3 args=[$WS/build/httpd.py] cwd=$WS/build ready={port:8000} persist=true
open : browser open url=http://127.0.0.1:8000/index.html viewport={width:1600,height:900}
ready : document.title matches / - Tracy Profiler/ (see SKILL.md)
EOF

View File

@@ -6,7 +6,7 @@
"${workspaceFolder}/import",
"${workspaceFolder}/merge",
"${workspaceFolder}/update",
"${workspaceFolder}/test",
"${workspaceFolder}/tests/tracy",
"${workspaceFolder}",
],
"cmake.buildDirectory": "${sourceDirectory}/build",

View File

@@ -121,12 +121,12 @@ set_option(TRACY_NO_CODE_TRANSFER "Disable collection of source code" OFF TracyC
set_option(TRACY_NO_CONTEXT_SWITCH "Disable capture of context switches" OFF TracyClient)
set_option(TRACY_NO_EXIT "Client executable does not exit until all profile data is sent to server" OFF TracyClient)
set_option(TRACY_NO_SAMPLING "Disable call stack sampling" OFF TracyClient)
set_option(TRACY_NO_WAIT_STACKS "Disable capture of context switch call stacks" OFF TracyClient)
set_option(TRACY_NO_VERIFY "Disable zone validation for C API" OFF TracyClient)
set_option(TRACY_NO_VSYNC_CAPTURE "Disable capture of hardware Vsync events" OFF TracyClient)
set_option(TRACY_NO_FRAME_IMAGE "Disable the frame image support and its thread" OFF TracyClient)
set_option(TRACY_NO_SYSTEM_TRACING "Disable systrace sampling" OFF TracyClient)
set_option(TRACY_PATCHABLE_NOPSLEDS "Enable nopsleds for efficient patching by system-level tools (e.g. rr)" OFF TracyClient)
set_option(TRACY_DELAYED_INIT "Enable delayed initialization of the library (init on first call)" OFF TracyClient)
set_option(TRACY_MANUAL_LIFETIME "Enable the manual lifetime management of the profile" OFF TracyClient)
set_option(TRACY_FIBERS "Enable fibers support" OFF TracyClient)
set_option(TRACY_NO_CRASH_HANDLER "Disable crash handling" OFF TracyClient)
@@ -136,7 +136,8 @@ set_option(TRACY_LIBUNWIND_BACKTRACE "Use libunwind backtracing where supported"
set_option(TRACY_SYMBOL_OFFLINE_RESOLVE "Instead of full runtime symbol resolution, only resolve the image path and offset to enable offline symbol resolution" OFF TracyClient)
set_option(TRACY_LIBBACKTRACE_ELF_DYNLOAD_SUPPORT "Enable libbacktrace to support dynamically loaded elfs in symbol resolution resolution after the first symbol resolve operation" OFF TracyClient)
set_option(TRACY_DEBUGINFOD "Enable debuginfod support" OFF TracyClient)
set_option(TRACY_IGNORE_MEMORY_FAULTS "Ignore instrumentation errors from memory free events that do not have a matching allocation" OFF TracyClient)
set_option(TRACY_IGNORE_MEMORY_FAULTS "Ignore instrumentation errors from memory free events that do not have a matching allocation, or from repeated allocations of the same address" OFF TracyClient)
set_option(TRACY_OPENGL_AUTO_CALIBRATION "Periodically recalibrate OpenGL GPU/CPU clock drift (forces a CPU/GPU sync each time)" OFF TracyClient)
# advanced
set_option(TRACY_VERBOSE "[advanced] Verbose output from the profiler" OFF TracyClient)
@@ -151,9 +152,6 @@ if(rocprofiler-sdk_FOUND)
endif()
# handle incompatible combinations
if(TRACY_MANUAL_LIFETIME AND NOT TRACY_DELAYED_INIT)
message(FATAL_ERROR "TRACY_MANUAL_LIFETIME can not be activated with disabled TRACY_DELAYED_INIT")
endif()
if(TRACY_DISALLOW_HW_TIMER AND NOT TRACY_TIMER_FALLBACK)
message(FATAL_ERROR "TRACY_DISALLOW_HW_TIMER can not be activated with disabled TRACY_TIMER_FALLBACK")
endif()
@@ -181,7 +179,8 @@ set(tracy_includes
${TRACY_PUBLIC_DIR}/tracy/TracyMetal.hmm
${TRACY_PUBLIC_DIR}/tracy/TracyOpenCL.hpp
${TRACY_PUBLIC_DIR}/tracy/TracyOpenGL.hpp
${TRACY_PUBLIC_DIR}/tracy/TracyVulkan.hpp)
${TRACY_PUBLIC_DIR}/tracy/TracyVulkan.hpp
${TRACY_PUBLIC_DIR}/tracy/TracyWebGPU.hpp)
set(client_includes
${TRACY_PUBLIC_DIR}/client/tracy_concurrentqueue.h
@@ -212,15 +211,19 @@ set(common_includes
${TRACY_PUBLIC_DIR}/common/TracyAlign.hpp
${TRACY_PUBLIC_DIR}/common/TracyAlloc.hpp
${TRACY_PUBLIC_DIR}/common/TracyApi.h
${TRACY_PUBLIC_DIR}/common/TracyAssert.hpp
${TRACY_PUBLIC_DIR}/common/TracyColor.hpp
${TRACY_PUBLIC_DIR}/common/TracyForceInline.hpp
${TRACY_PUBLIC_DIR}/common/TracyFormat.h
${TRACY_PUBLIC_DIR}/common/TracyMutex.hpp
${TRACY_PUBLIC_DIR}/common/TracyProtocol.hpp
${TRACY_PUBLIC_DIR}/common/TracyQueue.hpp
${TRACY_PUBLIC_DIR}/common/TracySocket.hpp
${TRACY_PUBLIC_DIR}/common/TracyStackFrames.hpp
${TRACY_PUBLIC_DIR}/common/TracyString.hpp
${TRACY_PUBLIC_DIR}/common/TracySystem.hpp
${TRACY_PUBLIC_DIR}/common/TracyTaggedUserlandAddress.hpp
${TRACY_PUBLIC_DIR}/common/TracyVersion.hpp
${TRACY_PUBLIC_DIR}/common/TracyWinFamily.hpp
${TRACY_PUBLIC_DIR}/common/TracyYield.hpp)
@@ -273,6 +276,11 @@ configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/TracyConfig.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/TracyConfigVersion.cmake"
COMPATIBILITY SameMinorVersion)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/TracyConfigVersion.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
option(TRACY_CLIENT_PYTHON "Whether to build Tracy python client library" OFF)

View File

@@ -1,5 +1,7 @@
@PACKAGE_INIT@
set(TRACY_VERSION_STRING "@TRACY_VERSION_STRING@")
include(CMakeFindDependencyMacro)
find_dependency(Threads REQUIRED)

185
NEWS
View File

@@ -2,12 +2,69 @@ Note: There is no guarantee that version mismatched client and server will
be able to talk with each other. Network protocol breakages won't be listed
here.
vx.xx.x (2026-xx-xx)
v0.14.1 (2026-08-22)
--------------------
- Dropped support for loading pre-0.11 traces.
- Silenced MSVC warning C4366: The result of the unary '&' operator may be
unaligned.
- Fixed section setup in on-demand mode.
- Hardened string length encoding in protocol.
- Fixed compilation failure related to VSync when targeting 32-bit on
Windows.
- Added TRACY_NO_WAIT_STACKS compile time macro.
- Improved documentation of requirements in edge cases:
- Memory allocation reporting needs to be done under a lock.
- Manual lifetime management requires shutdown of the profiler.
- Documented how zones behave when crossing the on-demand connection
boundary.
- Mention that statistics time range limit also affects source view.
- Memory events are now attributed to virtual fiber threads.
- Added TRACY_ASSERT macro allowing user-defined handling of asserts in the
profiler.
- Fixed csvexport options: -t, --sep, --filter.
- Configuration of plot color now matches the documented RGB channels order.
- Link profiler executable with libdl (required by old glibc versions).
- Improve coverage of link-time macro mismatch detection.
- Removed TRACY_DELAYED_INIT option. It was no-op since 2020.
- Fixed illegal instruction in rpmalloc on ARMv6 and older.
- Fixed handling of source code queries in no-callstack builds. Previously
the profiling session would hang at exit.
- Hardened Lua on-demand handling.
- Fixed system tracing on Linux kernels older than 4.1.
- Linux system tracing setup failures are now reported via Tracy debug
message functionality.
- Improved version handling in CMake / Meson / Python build systems.
- Fixed D3D12 readback buffer error.
- Child zone list in zone info window can be now sorted by name or by time.
- File dialog failures (e.g. related to the XDG portal) are now reported
in the modal dialog.
- Fixed NO_FILESELECTOR builds.
- Fixed possible profiler hang when there was not enough space to draw
a plot on the timeline.
- Update check can be now interrupted, e.g. when exiting the profiler.
- Patched the file selector library to use modern Wayland protocol for
setting parent-child window relation.
v0.14.0 (2026-08-09)
--------------------
- API break: removed "secure" variants of memory alloc and free macros. The
secure code path is now always enabled. Migrate by removing "Secure" from
the macros you use, e.g. TracySecureAlloc(...) -> TracyAlloc(...).
- Added printf-style variants of the C API zone text and name macros:
TracyCZoneTextF and TracyCZoneNameF now accept a format string and
arguments, instead of a fixed-length text buffer.
- Added tracy-capture-daemon for automated multi-client trace capture.
- Added tracy-merge utility for combining multiple trace files into one.
- Added support for Windows on ARM64 with MSVC.
- Added support for WebGPU.
- Binary releases are now also provided for macOS (needs manual quarantine
handling) and Linux (AppImage with Ubuntu 24.04 base, requires Wayland).
- Trace-specific settings storage has been completely overhauled. It is now
possible to make the settings sidecar file public, saved next to the trace
file.
- External frames are now omitted in the single-line call stack list visible
in messages list, or in memory allocation info window.
- External frames are now hidden by default in various contexts where they
@@ -15,9 +72,44 @@ vx.xx.x (2026-xx-xx)
- Flame graph window.
- Call stack window.
- Statistics window (sampling mode).
- Wait stacks have a separate setting for external frames, as you would
typically want to know what caused the wait state, which can be inferred
from the external code.
- External frames are now dimmed out in call stacks in various parts of UI.
- Sample markers on the timeline are now colored according to sample type:
- Green for samples in the profiled program,
- Blue for samples in external code,
- Red for samples in kernel.
- Single-line call stacks now have ellipsis at the end, if there are frames
remaining.
- Sample entry stacks window can now also show the stacks by which a symbol
was reached, i.e. it was present anywhere on the call stack, not only when
it was executing. Recursive re-entries can be either ignored or counted
separately.
- Entry stacks are now also available for symbols which were only reached,
but never directly sampled, e.g. in the assembly view of a call site with
purely child sample costs, or in the statistics window for symbols which
only have inclusive counts.
- Fixed inclusive sample counts of symbols with inline functions. Each
sample was counted multiple times: once for the symbol itself and once for
every of its inline functions present on the call stack.
- Fixed context switch samples leaking into per-symbol sample lists and
child sample data on trace load. This inflated range-limited sample counts
and child call costs, and made loaded traces disagree with live sessions,
where such samples are excluded from sampling statistics.
- Fixed wrong results of range-limited child call queries during live
capture, when some samples had to wait for context switch data to arrive.
- Fixed crashes when opening the sample entry stacks window for symbols
without any sampling data.
- Sampling statistics percentages are now calculated against the number of
samples that were actually collected. Previously, enabling the range
filter switched the calculation to a theoretical single-thread estimate,
which changed both the meaning and the scale of the displayed values.
- Find zone sample count percentages are now relative to the matched zones,
consistent with the time percentages in the same list, instead of being
relative to the whole trace.
- Context switch samples are now consistently excluded from all sampling
statistics views.
- System tracing on Windows has been refactored to be more robust.
- Tracing on Arm macOS will now have more precise timer readings.
- Extended CUDA support to track some previously missing memory operations.
@@ -29,7 +121,8 @@ vx.xx.x (2026-xx-xx)
- Added inline call stack list to the zone information window.
- The "call stack" button for opening the call stack in a separate window
is still there.
- Call stacks are now also displayed in zone tooltips (single-line).
- Call stacks are now also displayed in zone and memory allocation tooltips
(single-line).
- Implemented heuristic reconstruction of zone call stacks for zones that
were captured without call stacks.
- Requires sampling to be enabled, and at least one sample in the zone
@@ -71,13 +164,16 @@ vx.xx.x (2026-xx-xx)
- Chat topic description is now provided, based on the first user question.
- Each assistant reply is now labeled with used model and reply time.
- Follow-up questions can be automatically suggested.
- It is now possible to alter the assistant personality.
- Expanded LLM attachments.
- You can now attach complete symbol assembly.
- Entry call stacks can be now attached (previously it was only regular
call stacks).
- Crash call stack attachments are now annotated with crash info.
- Wait stack attachments now contain wait metadata.
- Source code can be attached (also with execution costs in symbol view).
- Zone histogram data can be attached for analysis.
- Added MCP server.
- Markdown renderer improvements.
- Tables are now properly rendered.
- Tasklist rendering has been implemented.
@@ -101,6 +197,7 @@ vx.xx.x (2026-xx-xx)
locations won't be fixed.
- Call stack window will now display notification if viewing a crash call
stack.
- Call stack window will now show a proper label if viewing a wait stack.
- Removal of Tracy crash handler stack from the reported crash call stack
should now work again on Linux.
- In disassembly line view, source file names are now displayed instead of
@@ -112,6 +209,8 @@ vx.xx.x (2026-xx-xx)
- Added TRACY_DISALLOW_HW_TIMER define for virtualized environments and WSL2,
which may not have reliable access to hardware timer registers. Falls back
to standard library timer with reduced resolution.
- Fixed TRACY_NO_CODE_TRANSFER define. It didn't prevent source code transfer
before.
- Fixed DPI scaling on macOS.
- Thread names are no longer truncated to 15 characters on Apple.
- Executable path is now inspected when looking for PDB files on Windows.
@@ -141,14 +240,20 @@ vx.xx.x (2026-xx-xx)
- Mismatched versions will break at linking.
- As a reminder, Tracy *always* required using the same set of compilation
options for the entire program.
- Message windows will now properly show full message in a tooltip for
multi-line messages.
- Message windows were improved.
- Full message will be now displayed in a tooltip for multi-line messages.
- Added right click context menu with option to copy message to clipboard.
- Greatly improved the in-profiler user manual.
- There is now chapter tree and the manual contents are displayed section
by section.
- Links to chapters are now properly working.
- The "bclogo" blocks are now correctly processed.
- The "bclogo" blocks are now correctly processed and displayed as proper
admonitions.
- The font awesome icons now show as in the rest of the UI.
- Footnotes are now rendered as proper footnotes.
- Tables are now rendered as intended.
- LaTeX math is now converted to readable form.
- Added a button to download the full PDF manual to the user manual window.
- Call stack window will now show the thread viewed call stack originates
from (if possible).
- "Visible threads" checkboxes in messages, flame graph and wait stacks
@@ -171,7 +276,75 @@ vx.xx.x (2026-xx-xx)
- The profiler can no longer be built with the statistics disabled.
- Fixed NVCC builds.
- Fixed possible lockups in Vulkan timer calibration loop.
- The flame graph view now supports zooming in and panning with the mouse.
- Flame graph improvements.
- The flame graph view now supports zooming in and panning with the mouse.
- Sampling flame graph items can be now also grouped by name, in addition
to symbol address.
- Sampling flame graph no longer omits display of unknown symbols. These
are now aggregated into blocks named after the executable image.
- General application crash information polish in the profiler UI.
- The achievements system has been converted to use markdown renderer.
- Offline symbol resolution with the update utility now supports custom
addr2line-compatible tools via -a and -A command line parameters.
Additionally, it is now possible to reset all call stack frame symbols to
unresolved with the -R parameter.
- Periodic recalibration of the clock drift in OpenGL contexts can be enabled
with the TRACY_OPENGL_AUTO_CALIBRATION compilation define. Note that this
requires a full CPU/GPU sync on each calibration event. These events will
not fire more often than once every second.
- Additional hardening of OpenGL tracing.
- Added missing C API for shared locks.
- Implemented semi-unique, nonsense random name generator.
- Can be used to set a trace description.
- Will be used to provide default description for newly added annotations.
- Polished look and feel of annotation regions on the timeline.
- Annotations can now be hidden.
- It is now possible to create annotations from time range limits.
- Expanded available tests.
- The previously existing "test" program now lives in "tests/tracy".
- The tests directory also contains some repro cases for solved problems.
- Expanded examples showing how to use the profiler.
- Includes "Dyna.net", a small 2D game that is a variation of Bomberman.
- Made numeric input field for trace parameters usable again.
- Added new type of trace parameter that exposes a trigger button.
- Added "sections" feature for marking high-level sections of program
execution.
- For example, a game with multiple levels may have each of the levels
marked as a section.
- Each section is a distinct region not restricted by other sections.
- Multiple sections can overlap.
- Sections can be assigned to user-defined categories.
- Can be used to set time range limits.
- Exposed via Lua bindings.
- Find zone window now also displays coefficient of variation, in addition
to standard deviation.
- Moved frame statistics from trace information window to its own window.
- Added various statistical data to bring frame statistics up to par with
the find zone window.
- Added a new time range limit for frame statistics. This accompanies the
previously existing "limit to view" option.
- More robust statistics for functions that recursively call themselves.
- General uplift of the compare traces window.
- Added all the statistical parameters displayed in find zone window that
were missing here.
- Frame comparison now also displays FPS values.
- The "more" and "less" indicators are now color coded to show their
importance. The amount of color these keywords are printed with is now
dependant on how divergent the numeric values are.
- It is now possible to limit time range for both traces that are compared
to their respective annotations or sections.
- Frame image playback improvements.
- Playback can be looped.
- The range of played frame images can be limited, either manually, or by
copying annotation, section, or time range limit. There's an "require
frame coverage" option to only include frames that are at least 75%
within the source region.
- Most windows now have a minimum width, in order to make sure all controls
provided in the window are visible. Previously it was easy to oversee some
button that was out of the window draw area.
- The ignore memory faults functionality (enabled by default if profiling
programs on Apple platforms) will now also ignore allocating memory at the
same location twice, without an intermediate free. Thanks, Tim Apple!
v0.13.1 (2025-12-11)

View File

@@ -4,7 +4,7 @@
### A real time, nanosecond resolution, remote telemetry, hybrid frame and sampling profiler for games and other applications.
Tracy supports profiling CPU (Direct support is provided for C, C++, Lua, Python and Fortran integration. At the same time, third-party bindings to many other languages exist on the internet, such as [Rust](https://github.com/nagisa/rust_tracy_client), [Zig](https://github.com/tealsnow/zig-tracy), [C#](https://github.com/clibequilibrium/Tracy-CSharp), [OCaml](https://github.com/imandra-ai/ocaml-tracy), [Odin](https://github.com/oskarnp/odin-tracy), etc.), GPU (All major graphic APIs: OpenGL, Vulkan, Direct3D 11/12, Metal, OpenCL, CUDA.), memory allocations, locks, context switches, automatically attribute screenshots to captured frames, and much more.
Tracy supports profiling CPU (Direct support is provided for C, C++, Lua, Python and Fortran integration. At the same time, third-party bindings to many other languages exist on the internet, such as [Rust](https://github.com/nagisa/rust_tracy_client), [Zig](https://github.com/tealsnow/zig-tracy), [C#](https://github.com/clibequilibrium/Tracy-CSharp), [OCaml](https://github.com/imandra-ai/ocaml-tracy), [Odin](https://github.com/oskarnp/odin-tracy), etc.), GPU (All major graphics/compute APIs: OpenGL, Vulkan, Direct3D 11/12, Metal, OpenCL, CUDA, WebGPU.), memory allocations, locks, context switches, automatically attribute screenshots to captured frames, and much more.
- [Documentation](https://github.com/wolfpld/tracy/releases/latest/download/tracy.pdf) for usage and build process instructions
- [Releases](https://github.com/wolfpld/tracy/releases) containing the documentation (`tracy.pdf`) and compiled Windows x64 binaries (`Tracy-<version>.7z`) as assets

31
capture/CMakePresets.json Normal file
View File

@@ -0,0 +1,31 @@
{
"version": 10,
"configurePresets": [
{
"name": "common",
"hidden": true,
"binaryDir": "${sourceDir}/build-${presetName}",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "debug",
"displayName": "Debug configuration",
"inherits": "common",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"displayName": "Release configuration",
"inherits": "common",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}

View File

@@ -137,6 +137,7 @@ int main( int argc, char** argv )
{
worker.Disconnect();
s_disconnect.store(false, std::memory_order_relaxed );
while( worker.IsConnected() ) std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
break;
}

View File

@@ -168,7 +168,13 @@ void CaptureThread( ClientSession* session, const std::string& address, uint16_t
std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
}
if( worker.IsConnected() )
{
worker.Disconnect();
while( worker.IsConnected() ) std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
}
printf( "\nSaving %s...", outputFile.c_str() );
fflush( stdout );

View File

@@ -0,0 +1,13 @@
diff --git i/imconfig.h w/imconfig.h
index b40db3898..561ab7782 100644
--- i/imconfig.h
+++ w/imconfig.h
@@ -42,7 +42,7 @@
//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW)
//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a)
//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME).
-//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default).
+#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default).
//#define IMGUI_DISABLE_TIME_FUNCTIONS // Don't setup default platform_io.Platform_SessionDate value using time(), localtime_r().
//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")).
//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf)

View File

@@ -0,0 +1,13 @@
diff --git i/imconfig.h w/imconfig.h
index b40db3898..f7f864394 100644
--- i/imconfig.h
+++ w/imconfig.h
@@ -50,7 +50,7 @@
//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies)
//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function.
//#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions().
-//#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded fonts (ProggyClean + ProggyForever). Remove ~9 KB + ~14 KB from output binary. AddFontDefaultXXX() functions will assert.
+#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded fonts (ProggyClean + ProggyForever). Remove ~9 KB + ~14 KB from output binary. AddFontDefaultXXX() functions will assert.
//#define IMGUI_DISABLE_DEFAULT_FONT_BITMAP // Disable default embedded bitmap font (ProggyClean). Remove ~9 KB from output binary. AddFontDefaultBitmap() will assert.
//#define IMGUI_DISABLE_DEFAULT_FONT_VECTOR // Disable default embedded vector font (ProggyForever), Remove ~14 KB from output binary. AddFontDefaultVector() will assert.
//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available

View File

@@ -0,0 +1,13 @@
diff --git a/backends/imgui_impl_opengl3.cpp b/backends/imgui_impl_opengl3.cpp
index a9e32b7ac..2cdbc4812 100644
--- a/backends/imgui_impl_opengl3.cpp
+++ b/backends/imgui_impl_opengl3.cpp
@@ -1069,7 +1069,7 @@ bool ImGui_ImplOpenGL3_Init(const char* glsl_version)
bd->HasPolygonMode = (!bd->GlProfileIsES2 && !bd->GlProfileIsES3);
#endif
#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER
- bd->HasBindSampler = (bd->GlVersion >= 330 || bd->GlProfileIsES3);
+ //bd->HasBindSampler = (bd->GlVersion >= 330 || bd->GlProfileIsES3);
#endif
bd->HasClipOrigin = (bd->GlVersion >= 450);
#ifdef IMGUI_IMPL_OPENGL_HAS_EXTENSIONS

View File

@@ -0,0 +1,14 @@
diff --git a/imgui_tables.cpp b/imgui_tables.cpp
--- a/imgui_tables.cpp
+++ b/imgui_tables.cpp
@@ -4082,7 +4082,9 @@
}
if (load_flags & ImGuiTableFlags_Reorderable)
column->DisplayOrder = column_settings->DisplayOrder;
- else
+ else if (column_settings->Index != -1)
+ // An Index of -1 means this column had no "Column N" line in the .ini file (columns without saved
+ // data are not written). Keep the display order as initialized (identity for non-reorderable tables). (#9519)
column->DisplayOrder = column_settings->Index; // Because default depends on previous Index, we need to set that up and cannot rely on TableInitColumnDefaults()
if ((load_flags & ImGuiTableFlags_Hideable) && column_settings->IsEnabled != -1)
column->IsUserEnabled = column->IsUserEnabledNextFrame = (column_settings->IsEnabled == 1);

View File

@@ -0,0 +1,203 @@
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d6a9edc..abef8d7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -32,18 +32,18 @@ if(nfd_PLATFORM STREQUAL PLATFORM_LINUX)
if(NFD_WAYLAND)
pkg_check_modules(WAYLAND REQUIRED wayland-client)
message(STATUS "Using Wayland version: ${WAYLAND_VERSION}")
- set(NFD_WAYLAND_PROTOCOL_XDG_FOREIGN ${CMAKE_CURRENT_SOURCE_DIR}/../3ps/wayland-protocols/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml)
+ set(NFD_WAYLAND_PROTOCOL_XDG_FOREIGN ${CMAKE_CURRENT_SOURCE_DIR}/../3ps/wayland-protocols/unstable/xdg-foreign/xdg-foreign-unstable-v2.xml)
add_custom_command(
- OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.h
- COMMAND wayland-scanner client-header < ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN} > ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.h
+ OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v2.h
+ COMMAND wayland-scanner client-header < ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN} > ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v2.h
MAIN_DEPENDENCY ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN}
)
add_custom_command(
- OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.c
- COMMAND wayland-scanner private-code < ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN} > ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.c
+ OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v2.c
+ COMMAND wayland-scanner private-code < ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN} > ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v2.c
MAIN_DEPENDENCY ${NFD_WAYLAND_PROTOCOL_XDG_FOREIGN}
)
- list(APPEND SOURCE_FILES ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.h ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v1.c)
+ list(APPEND SOURCE_FILES ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v2.h ${CMAKE_CURRENT_BINARY_DIR}/xdg-foreign-unstable-v2.c)
endif()
endif()
diff --git a/src/nfd_gtk.cpp b/src/nfd_gtk.cpp
index 76fdcd7..cec691d 100644
--- a/src/nfd_gtk.cpp
+++ b/src/nfd_gtk.cpp
@@ -433,17 +433,17 @@ gint RunDialogWithFocus(GtkDialog* dialog) {
#if defined(NFD_WAYLAND)
void DestroyXdgExported(void* context) {
- zxdg_exported_v1_destroy(static_cast<struct zxdg_exported_v1*>(context));
+ zxdg_exported_v2_destroy(static_cast<struct zxdg_exported_v2*>(context));
}
-void zxdg_exported_v1_handle(void* context, struct zxdg_exported_v1*, const char* handle) {
+void zxdg_exported_v2_handle(void* context, struct zxdg_exported_v2*, const char* handle) {
GdkWindow* childWindow = static_cast<GdkWindow*>(context);
// GDK doesn't modify the string, even though it takes char*, so we can cast away the constness
gdk_wayland_window_set_transient_for_exported(childWindow, const_cast<char*>(handle));
}
-constexpr struct zxdg_exported_v1_listener wayland_xdg_exported_v1_listener{
- &zxdg_exported_v1_handle};
+constexpr struct zxdg_exported_v2_listener wayland_xdg_exported_v2_listener{
+ &zxdg_exported_v2_handle};
#endif
// This is an RAII class that wraps the parenting of a GtkWidget (the file dialog).
@@ -645,17 +645,17 @@ struct NativeWindowParenter {
#if defined(NFD_WAYLAND)
void SetParentWayland(GdkWindow* childWindow) {
- if (wayland_display && wayland_xdg_exporter_v1) {
- struct zxdg_exported_v1* exported = zxdg_exporter_v1_export(
- wayland_xdg_exporter_v1, static_cast<struct wl_surface*>(parentWindowHandle));
+ if (wayland_display && wayland_xdg_exporter_v2) {
+ struct zxdg_exported_v2* exported = zxdg_exporter_v2_export_toplevel(
+ wayland_xdg_exporter_v2, static_cast<struct wl_surface*>(parentWindowHandle));
if (!exported) {
// if we fail to export the wl_surface, act as if the window has no parent
return;
}
- zxdg_exported_v1_add_listener(
- exported, &wayland_xdg_exported_v1_listener, static_cast<void*>(childWindow));
+ zxdg_exported_v2_add_listener(
+ exported, &wayland_xdg_exported_v2_listener, static_cast<void*>(childWindow));
wl_display_roundtrip(wayland_display);
- zxdg_exported_v1_set_user_data(exported, nullptr);
+ zxdg_exported_v2_set_user_data(exported, nullptr);
destroy.fn = &DestroyXdgExported;
destroy.context = static_cast<void*>(exported);
}
diff --git a/src/nfd_linux_shared.hpp b/src/nfd_linux_shared.hpp
index 1413bac..e826f1e 100644
--- a/src/nfd_linux_shared.hpp
+++ b/src/nfd_linux_shared.hpp
@@ -14,7 +14,7 @@
#ifdef NFD_WAYLAND
#include <wayland-client.h>
-#include "xdg-foreign-unstable-v1.h"
+#include "xdg-foreign-unstable-v2.h"
#endif
namespace {
@@ -81,9 +81,9 @@ nfdnchar_t* emit_case_insensitive_glob(const nfdnchar_t* begin,
#ifdef NFD_WAYLAND
struct wl_display* wayland_display;
struct wl_registry* wayland_registry;
-uint32_t wayland_xdg_exporter_v1_name;
-struct zxdg_exporter_v1* wayland_xdg_exporter_v1;
-constexpr const char* XDG_EXPORTER_V1 = "zxdg_exporter_v1";
+uint32_t wayland_xdg_exporter_v2_name;
+struct zxdg_exporter_v2* wayland_xdg_exporter_v2;
+constexpr const char* XDG_EXPORTER_V2 = "zxdg_exporter_v2";
#endif
void EmptyFn(void*) {}
@@ -103,19 +103,19 @@ void registry_handle_global(void* context,
uint32_t version) {
(void)context;
(void)version;
- if (strcmp(interface, XDG_EXPORTER_V1) == 0) {
- wayland_xdg_exporter_v1_name = name;
- wayland_xdg_exporter_v1 = static_cast<struct zxdg_exporter_v1*>(wl_registry_bind(
- registry, name, &zxdg_exporter_v1_interface, zxdg_exporter_v1_interface.version));
+ if (strcmp(interface, XDG_EXPORTER_V2) == 0) {
+ wayland_xdg_exporter_v2_name = name;
+ wayland_xdg_exporter_v2 = static_cast<struct zxdg_exporter_v2*>(wl_registry_bind(
+ registry, name, &zxdg_exporter_v2_interface, zxdg_exporter_v2_interface.version));
}
}
void registry_handle_global_remove(void* context, struct wl_registry* registry, uint32_t name) {
(void)context;
(void)registry;
- if (wayland_xdg_exporter_v1 && name == wayland_xdg_exporter_v1_name) {
- zxdg_exporter_v1_destroy(wayland_xdg_exporter_v1);
- wayland_xdg_exporter_v1 = nullptr;
+ if (wayland_xdg_exporter_v2 && name == wayland_xdg_exporter_v2_name) {
+ zxdg_exporter_v2_destroy(wayland_xdg_exporter_v2);
+ wayland_xdg_exporter_v2 = nullptr;
}
}
@@ -128,7 +128,7 @@ void NFD_Wayland_Init(void) {
void NFD_Wayland_Quit(void) {
if (wayland_display) {
- if (wayland_xdg_exporter_v1) zxdg_exporter_v1_destroy(wayland_xdg_exporter_v1);
+ if (wayland_xdg_exporter_v2) zxdg_exporter_v2_destroy(wayland_xdg_exporter_v2);
wl_registry_destroy(wayland_registry);
}
}
@@ -143,7 +143,7 @@ nfdresult_t NFD_SetWaylandDisplay(wl_display* display) {
wayland_display = display;
if (wayland_display) {
wayland_registry = wl_display_get_registry(wayland_display);
- wayland_xdg_exporter_v1 = nullptr;
+ wayland_xdg_exporter_v2 = nullptr;
// seems like registry can't be null
wl_registry_add_listener(wayland_registry, &wayland_registry_listener, nullptr);
wl_display_roundtrip(wayland_display);
diff --git a/src/nfd_portal.cpp b/src/nfd_portal.cpp
index df840e0..e66ca7f 100644
--- a/src/nfd_portal.cpp
+++ b/src/nfd_portal.cpp
@@ -148,10 +148,10 @@ constexpr const char* DBUS_REQUEST_IFACE = "org.freedesktop.portal.Request";
constexpr const char* WAYLAND_PREFIX = "wayland:";
void DestroyXdgExported(void* context) {
- zxdg_exported_v1_destroy(static_cast<struct zxdg_exported_v1*>(context));
+ zxdg_exported_v2_destroy(static_cast<struct zxdg_exported_v2*>(context));
}
-void zxdg_exported_v1_handle(void* context, struct zxdg_exported_v1*, const char* handle) {
+void zxdg_exported_v2_handle(void* context, struct zxdg_exported_v2*, const char* handle) {
DBusMessageIter& iter = *static_cast<DBusMessageIter*>(context);
const size_t handle_len = strlen(handle);
const size_t prefix_len = strlen(WAYLAND_PREFIX);
@@ -163,8 +163,8 @@ void zxdg_exported_v1_handle(void* context, struct zxdg_exported_v1*, const char
NFDi_Free(buf);
}
-constexpr struct zxdg_exported_v1_listener wayland_xdg_exported_v1_listener{
- &zxdg_exported_v1_handle};
+constexpr struct zxdg_exported_v2_listener wayland_xdg_exported_v2_listener{
+ &zxdg_exported_v2_handle};
#endif
void AppendOpenFileQueryParentWindow(DBusMessageIter& iter,
@@ -194,18 +194,18 @@ void AppendOpenFileQueryParentWindow(DBusMessageIter& iter,
#endif
#ifdef NFD_WAYLAND
case NFD_WINDOW_HANDLE_TYPE_WAYLAND: {
- if (wayland_display && wayland_xdg_exporter_v1) {
- struct zxdg_exported_v1* exported = zxdg_exporter_v1_export(
- wayland_xdg_exporter_v1, static_cast<struct wl_surface*>(parentWindow.handle));
+ if (wayland_display && wayland_xdg_exporter_v2) {
+ struct zxdg_exported_v2* exported = zxdg_exporter_v2_export_toplevel(
+ wayland_xdg_exporter_v2, static_cast<struct wl_surface*>(parentWindow.handle));
if (!exported) {
// if we fail to export the wl_surface, act as if the window has no parent
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY);
return;
}
- zxdg_exported_v1_add_listener(
- exported, &wayland_xdg_exported_v1_listener, static_cast<void*>(&iter));
+ zxdg_exported_v2_add_listener(
+ exported, &wayland_xdg_exported_v2_listener, static_cast<void*>(&iter));
wl_display_roundtrip(wayland_display);
- zxdg_exported_v1_set_user_data(exported, nullptr);
+ zxdg_exported_v2_set_user_data(exported, nullptr);
destroy.fn = &DestroyXdgExported;
destroy.context = static_cast<void*>(exported);
return;

View File

@@ -8,11 +8,20 @@ set (ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../")
include(FindPkgConfig)
include(${CMAKE_CURRENT_LIST_DIR}/CPM.cmake)
if(APPLE AND BUNDLE)
set(DLOPT ON)
else()
set(DLOPT OFF)
endif()
option(DOWNLOAD_CAPSTONE "Force download capstone" ON)
option(DOWNLOAD_GLFW "Force download glfw" OFF)
option(DOWNLOAD_FREETYPE "Force download freetype" OFF)
option(DOWNLOAD_LIBCURL "Force download libcURL" OFF)
option(DOWNLOAD_PUGIXML "Force download pugixml" OFF)
if(VENDOR_GUI)
option(DOWNLOAD_GLFW "Force download glfw" ${DLOPT})
option(DOWNLOAD_FREETYPE "Force download freetype" ${DLOPT})
option(DOWNLOAD_LIBCURL "Force download libcURL" OFF)
option(DOWNLOAD_PUGIXML "Force download pugixml" ${DLOPT})
endif()
# capstone
@@ -26,7 +35,7 @@ else()
CPMAddPackage(
NAME capstone
GITHUB_REPOSITORY capstone-engine/capstone
GIT_TAG 6.0.0-Alpha9
GIT_TAG 6.0.0-Alpha10
OPTIONS
"CAPSTONE_X86_ATT_DISABLE ON"
"CAPSTONE_ALPHA_SUPPORT OFF"
@@ -58,52 +67,6 @@ else()
target_link_libraries(TracyCapstone INTERFACE capstone_static)
endif()
# GLFW
if(NOT USE_WAYLAND AND NOT EMSCRIPTEN)
pkg_check_modules(GLFW glfw3)
if (GLFW_FOUND AND NOT DOWNLOAD_GLFW)
add_library(TracyGlfw3 INTERFACE)
target_include_directories(TracyGlfw3 INTERFACE ${GLFW_INCLUDE_DIRS})
target_link_libraries(TracyGlfw3 INTERFACE ${GLFW_LINK_LIBRARIES})
else()
CPMAddPackage(
NAME glfw
GITHUB_REPOSITORY glfw/glfw
GIT_TAG 3.4
OPTIONS
"GLFW_BUILD_EXAMPLES OFF"
"GLFW_BUILD_TESTS OFF"
"GLFW_BUILD_DOCS OFF"
"GLFW_INSTALL OFF"
EXCLUDE_FROM_ALL TRUE
)
add_library(TracyGlfw3 INTERFACE)
target_link_libraries(TracyGlfw3 INTERFACE glfw)
endif()
endif()
# freetype
pkg_check_modules(FREETYPE freetype2)
if (FREETYPE_FOUND AND NOT DOWNLOAD_FREETYPE)
add_library(TracyFreetype INTERFACE)
target_include_directories(TracyFreetype INTERFACE ${FREETYPE_INCLUDE_DIRS})
target_link_libraries(TracyFreetype INTERFACE ${FREETYPE_LINK_LIBRARIES})
else()
CPMAddPackage(
NAME freetype
GITHUB_REPOSITORY freetype/freetype
GIT_TAG VER-2-14-3
OPTIONS
"FT_DISABLE_HARFBUZZ ON"
"FT_WITH_HARFBUZZ OFF"
EXCLUDE_FROM_ALL TRUE
)
add_library(TracyFreetype INTERFACE)
target_link_libraries(TracyFreetype INTERFACE freetype)
endif()
# Zstd
CPMAddPackage(
@@ -132,65 +95,6 @@ set(GETOPT_HEADERS ${GETOPT_DIR}/getopt.h)
add_library(TracyGetOpt STATIC EXCLUDE_FROM_ALL ${GETOPT_SOURCES} ${GETOPT_HEADERS})
target_include_directories(TracyGetOpt PUBLIC ${GETOPT_DIR})
# ImGui
CPMAddPackage(
NAME ImGui
GITHUB_REPOSITORY ocornut/imgui
GIT_TAG v1.92.8-docking
DOWNLOAD_ONLY TRUE
PATCHES
"${CMAKE_CURRENT_LIST_DIR}/imgui-emscripten.patch"
"${CMAKE_CURRENT_LIST_DIR}/imgui-loader.patch"
)
set(IMGUI_SOURCES
imgui_widgets.cpp
imgui_draw.cpp
imgui_demo.cpp
imgui.cpp
imgui_tables.cpp
misc/freetype/imgui_freetype.cpp
backends/imgui_impl_opengl3.cpp
)
list(TRANSFORM IMGUI_SOURCES PREPEND "${ImGui_SOURCE_DIR}/")
add_library(TracyImGui STATIC EXCLUDE_FROM_ALL ${IMGUI_SOURCES})
target_include_directories(TracyImGui PUBLIC ${ImGui_SOURCE_DIR})
target_link_libraries(TracyImGui PUBLIC TracyFreetype)
target_compile_definitions(TracyImGui PRIVATE "IMGUI_ENABLE_FREETYPE")
target_compile_definitions(TracyImGui PUBLIC "IMGUI_USE_WCHAR32")
#target_compile_definitions(TracyImGui PUBLIC "IMGUI_DISABLE_OBSOLETE_FUNCTIONS")
if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND LEGACY)
find_package(X11 REQUIRED)
target_link_libraries(TracyImGui PUBLIC ${X11_LIBRARIES})
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(TracyImGui PRIVATE "IMGUI_DISABLE_DEBUG_TOOLS" "IMGUI_DISABLE_DEMO_WINDOWS")
endif()
# NFD
if(NOT NO_FILESELECTOR AND NOT EMSCRIPTEN)
if(GTK_FILESELECTOR)
set(NFD_PORTAL OFF)
else()
set(NFD_PORTAL ON)
endif()
CPMAddPackage(
NAME nfd
GITHUB_REPOSITORY btzy/nativefiledialog-extended
GIT_TAG v1.3.0
EXCLUDE_FROM_ALL TRUE
OPTIONS
"NFD_PORTAL ${NFD_PORTAL}"
)
endif()
# PPQSort
CPMAddPackage(
@@ -212,93 +116,226 @@ CPMAddPackage(
EXCLUDE_FROM_ALL TRUE
)
# md4c
if(VENDOR_GUI)
CPMAddPackage(
NAME md4c
GITHUB_REPOSITORY mity/md4c
GIT_TAG 755ce49acdc7cd682d4502b4796db5ed6a1230fb
EXCLUDE_FROM_ALL TRUE
)
# GLFW
if(NOT EMSCRIPTEN)
# base64
set(BUILD_SHARED_LIBS_SAVE ${BUILD_SHARED_LIBS})
set(BUILD_SHARED_LIBS OFF)
CPMAddPackage(
NAME base64
GITHUB_REPOSITORY aklomp/base64
GIT_TAG v0.5.2
OPTIONS
"BASE64_BUILD_CLI OFF"
"BASE64_WITH_OpenMP OFF"
EXCLUDE_FROM_ALL TRUE
)
set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_SAVE})
# tidy
CPMAddPackage(
NAME tidy
GITHUB_REPOSITORY htacg/tidy-html5
GIT_TAG 5.8.0
PATCHES
"${CMAKE_CURRENT_LIST_DIR}/tidy-cmake.patch"
EXCLUDE_FROM_ALL TRUE
)
# usearch
CPMAddPackage(
NAME usearch
GITHUB_REPOSITORY unum-cloud/usearch
GIT_TAG v2.25.2
EXCLUDE_FROM_ALL TRUE
)
# pugixml
pkg_check_modules(PUGIXML pugixml)
if (PUGIXML_FOUND AND NOT DOWNLOAD_PUGIXML)
add_library(TracyPugixml INTERFACE)
target_include_directories(TracyPugixml INTERFACE ${PUGIXML_INCLUDE_DIRS})
target_link_libraries(TracyPugixml INTERFACE ${PUGIXML_LINK_LIBRARIES})
else()
CPMAddPackage(
NAME pugixml
GITHUB_REPOSITORY zeux/pugixml
GIT_TAG v1.15
EXCLUDE_FROM_ALL TRUE
)
add_library(TracyPugixml INTERFACE)
target_link_libraries(TracyPugixml INTERFACE pugixml)
if(NOT USE_WAYLAND AND NOT EMSCRIPTEN)
pkg_check_modules(GLFW glfw3)
if (GLFW_FOUND AND NOT DOWNLOAD_GLFW)
add_library(TracyGlfw3 INTERFACE)
target_include_directories(TracyGlfw3 INTERFACE ${GLFW_INCLUDE_DIRS})
target_link_libraries(TracyGlfw3 INTERFACE ${GLFW_LINK_LIBRARIES})
else()
CPMAddPackage(
NAME glfw
GITHUB_REPOSITORY glfw/glfw
GIT_TAG 3.4
OPTIONS
"GLFW_BUILD_EXAMPLES OFF"
"GLFW_BUILD_TESTS OFF"
"GLFW_BUILD_DOCS OFF"
"GLFW_INSTALL OFF"
EXCLUDE_FROM_ALL TRUE
)
add_library(TracyGlfw3 INTERFACE)
target_link_libraries(TracyGlfw3 INTERFACE glfw)
endif()
endif()
# libcurl
# freetype
pkg_check_modules(LIBCURL libcurl>=7.87.0)
if (LIBCURL_FOUND AND NOT DOWNLOAD_LIBCURL)
add_library(TracyLibcurl INTERFACE)
target_include_directories(TracyLibcurl INTERFACE ${LIBCURL_INCLUDE_DIRS})
target_link_libraries(TracyLibcurl INTERFACE ${LIBCURL_LINK_LIBRARIES})
pkg_check_modules(FREETYPE freetype2)
if (FREETYPE_FOUND AND NOT DOWNLOAD_FREETYPE)
add_library(TracyFreetype INTERFACE)
target_include_directories(TracyFreetype INTERFACE ${FREETYPE_INCLUDE_DIRS})
target_link_libraries(TracyFreetype INTERFACE ${FREETYPE_LINK_LIBRARIES})
else()
CPMAddPackage(
NAME libcurl
GITHUB_REPOSITORY curl/curl
GIT_TAG curl-8_20_0
NAME freetype
GITHUB_REPOSITORY freetype/freetype
GIT_TAG VER-2-14-3
OPTIONS
"BUILD_STATIC_LIBS ON"
"BUILD_SHARED_LIBS OFF"
"HTTP_ONLY ON"
"CURL_ZSTD OFF"
"CURL_USE_LIBPSL OFF"
"FT_DISABLE_HARFBUZZ ON"
"FT_WITH_HARFBUZZ OFF"
"FT_DISABLE_ZLIB ON"
"FT_DISABLE_BZIP2 ON"
"FT_DISABLE_PNG ON"
"FT_DISABLE_BROTLI ON"
EXCLUDE_FROM_ALL TRUE
)
add_library(TracyLibcurl INTERFACE)
target_link_libraries(TracyLibcurl INTERFACE libcurl_static)
target_include_directories(TracyLibcurl INTERFACE ${libcurl_SOURCE_DIR}/include)
add_library(TracyFreetype INTERFACE)
target_link_libraries(TracyFreetype INTERFACE freetype)
endif()
# ImGui
CPMAddPackage(
NAME ImGui
GITHUB_REPOSITORY ocornut/imgui
GIT_TAG v1.92.9b-docking
DOWNLOAD_ONLY TRUE
PATCHES
"${CMAKE_CURRENT_LIST_DIR}/imgui-emscripten.patch"
"${CMAKE_CURRENT_LIST_DIR}/imgui-loader.patch"
"${CMAKE_CURRENT_LIST_DIR}/imgui-no-samplers.patch"
"${CMAKE_CURRENT_LIST_DIR}/imgui-no-default-font.patch"
"${CMAKE_CURRENT_LIST_DIR}/imgui-macos-clipboard.patch"
"${CMAKE_CURRENT_LIST_DIR}/imgui-phantom-column.patch"
)
set(IMGUI_SOURCES
imgui_widgets.cpp
imgui_draw.cpp
imgui_demo.cpp
imgui.cpp
imgui_tables.cpp
misc/freetype/imgui_freetype.cpp
backends/imgui_impl_opengl3.cpp
)
list(TRANSFORM IMGUI_SOURCES PREPEND "${ImGui_SOURCE_DIR}/")
add_library(TracyImGui STATIC EXCLUDE_FROM_ALL ${IMGUI_SOURCES})
target_include_directories(TracyImGui PUBLIC ${ImGui_SOURCE_DIR})
target_link_libraries(TracyImGui PUBLIC
TracyFreetype
${CMAKE_DL_LIBS}
)
target_compile_definitions(TracyImGui PRIVATE "IMGUI_ENABLE_FREETYPE")
target_compile_definitions(TracyImGui PUBLIC "IMGUI_USE_WCHAR32")
#target_compile_definitions(TracyImGui PUBLIC "IMGUI_DISABLE_OBSOLETE_FUNCTIONS")
if (CMAKE_SYSTEM_NAME STREQUAL "Linux" AND LEGACY)
find_package(X11 REQUIRED)
target_link_libraries(TracyImGui PUBLIC ${X11_LIBRARIES})
endif()
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(TracyImGui PRIVATE "IMGUI_DISABLE_DEBUG_TOOLS" "IMGUI_DISABLE_DEMO_WINDOWS")
endif()
if(APPLE)
target_link_libraries(TracyImGui PUBLIC "-framework ApplicationServices")
endif()
# NFD
if(NOT NO_FILESELECTOR AND NOT EMSCRIPTEN)
if(GTK_FILESELECTOR)
set(NFD_PORTAL OFF)
else()
set(NFD_PORTAL ON)
endif()
CPMAddPackage(
NAME nfd
GITHUB_REPOSITORY btzy/nativefiledialog-extended
GIT_TAG 3cd252a8f7ca32419b1ca235c2990ba6a0ecba7c
EXCLUDE_FROM_ALL TRUE
PATCHES
"${CMAKE_CURRENT_LIST_DIR}/nfd-xdg-foreign-v2.patch"
OPTIONS
"BUILD_SHARED_LIBS OFF"
"NFD_PORTAL ${NFD_PORTAL}"
)
endif()
# md4c
CPMAddPackage(
NAME md4c
GITHUB_REPOSITORY mity/md4c
GIT_TAG 65c6c9d72cebd9a731aaa5597414ce04d9ea5de3
OPTIONS
"BUILD_SHARED_LIBS OFF"
EXCLUDE_FROM_ALL TRUE
)
if(NOT EMSCRIPTEN)
# base64
set(BUILD_SHARED_LIBS_SAVE ${BUILD_SHARED_LIBS})
set(BUILD_SHARED_LIBS OFF)
CPMAddPackage(
NAME base64
GITHUB_REPOSITORY aklomp/base64
GIT_TAG v0.5.2
OPTIONS
"BASE64_BUILD_CLI OFF"
"BASE64_WITH_OpenMP OFF"
EXCLUDE_FROM_ALL TRUE
)
set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_SAVE})
# tidy
CPMAddPackage(
NAME tidy
GITHUB_REPOSITORY htacg/tidy-html5
GIT_TAG 5.8.0
PATCHES
"${CMAKE_CURRENT_LIST_DIR}/tidy-cmake.patch"
EXCLUDE_FROM_ALL TRUE
)
# usearch
CPMAddPackage(
NAME usearch
GITHUB_REPOSITORY unum-cloud/usearch
GIT_TAG v2.26.0
EXCLUDE_FROM_ALL TRUE
)
# pugixml
pkg_check_modules(PUGIXML pugixml)
if (PUGIXML_FOUND AND NOT DOWNLOAD_PUGIXML)
add_library(TracyPugixml INTERFACE)
target_include_directories(TracyPugixml INTERFACE ${PUGIXML_INCLUDE_DIRS})
target_link_libraries(TracyPugixml INTERFACE ${PUGIXML_LINK_LIBRARIES})
else()
CPMAddPackage(
NAME pugixml
GITHUB_REPOSITORY zeux/pugixml
GIT_TAG v1.16
EXCLUDE_FROM_ALL TRUE
)
add_library(TracyPugixml INTERFACE)
target_link_libraries(TracyPugixml INTERFACE pugixml)
endif()
# libcurl
pkg_check_modules(LIBCURL libcurl>=7.87.0)
if (LIBCURL_FOUND AND NOT DOWNLOAD_LIBCURL)
add_library(TracyLibcurl INTERFACE)
target_include_directories(TracyLibcurl INTERFACE ${LIBCURL_INCLUDE_DIRS})
target_link_libraries(TracyLibcurl INTERFACE ${LIBCURL_LINK_LIBRARIES})
else()
CPMAddPackage(
NAME libcurl
GITHUB_REPOSITORY curl/curl
GIT_TAG curl-8_21_0
OPTIONS
"BUILD_STATIC_LIBS ON"
"BUILD_SHARED_LIBS OFF"
"HTTP_ONLY ON"
"CURL_ZSTD OFF"
"CURL_USE_LIBPSL OFF"
"CURL_USE_LIBSSH2 OFF"
"CURL_BROTLI OFF"
"USE_NGHTTP2 OFF"
"USE_LIBIDN2 OFF"
EXCLUDE_FROM_ALL TRUE
)
add_library(TracyLibcurl INTERFACE)
target_link_libraries(TracyLibcurl INTERFACE libcurl_static)
target_include_directories(TracyLibcurl INTERFACE ${libcurl_SOURCE_DIR}/include)
endif()
endif()
endif()

View File

@@ -0,0 +1,31 @@
{
"version": 10,
"configurePresets": [
{
"name": "common",
"hidden": true,
"binaryDir": "${sourceDir}/build-${presetName}",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "debug",
"displayName": "Debug configuration",
"inherits": "common",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"displayName": "Release configuration",
"inherits": "common",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}

View File

@@ -26,17 +26,17 @@ void print_usage_exit(int e)
fprintf(stderr, "Usage:\n");
fprintf(stderr, " extract [OPTION...] <trace file>\n");
fprintf(stderr, "\n");
fprintf(stderr, " -h, --help Print usage\n");
fprintf(stderr, " -V, --version Show version information\n");
fprintf(stderr, " -f, --filter arg Filter zone names (default: "")\n");
fprintf(stderr, " -s, --sep arg CSV separator (default: ,)\n");
fprintf(stderr, " -c, --case Case sensitive filtering\n");
fprintf(stderr, " -e, --self Get self times\n");
fprintf(stderr, " -u, --unwrap Report each cpu zone event\n");
fprintf(stderr, " -g, --gpu Report each gpu zone event\n" );
fprintf(stderr, " -m, --messages Report only messages\n");
fprintf(stderr, " -p, --plot Report plot data (only with -u)\n");
fprintf(stderr, " -t, --truncated_mean arg Report truncated mean (arg is the percentile. Default is 90)\n");
fprintf(stderr, " -h, --help Print usage\n");
fprintf(stderr, " -V, --version Show version information\n");
fprintf(stderr, " -f, --filter arg Filter zone names (default: "")\n");
fprintf(stderr, " -s, --sep arg CSV separator (default: ,)\n");
fprintf(stderr, " -c, --case Case sensitive filtering\n");
fprintf(stderr, " -e, --self Get self times\n");
fprintf(stderr, " -u, --unwrap Report each cpu zone event\n");
fprintf(stderr, " -g, --gpu Report each gpu zone event\n" );
fprintf(stderr, " -m, --messages Report only messages\n");
fprintf(stderr, " -p, --plot Report plot data (only with -u)\n");
fprintf(stderr, " -t, --truncated_mean[=arg] Report truncated mean (arg is the percentile. Default is 90)\n");
exit(e);
}
@@ -66,8 +66,8 @@ Args parse_args(int argc, char** argv)
struct option long_opts[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'V' },
{ "filter", optional_argument, NULL, 'f' },
{ "sep", optional_argument, NULL, 's' },
{ "filter", required_argument, NULL, 'f' },
{ "sep", required_argument, NULL, 's' },
{ "case", no_argument, NULL, 'c' },
{ "self", no_argument, NULL, 'e' },
{ "unwrap", no_argument, NULL, 'u' },
@@ -79,7 +79,7 @@ Args parse_args(int argc, char** argv)
};
int c;
while ((c = getopt_long(argc, argv, "hf:s:ceugmpV", long_opts, NULL)) != -1)
while ((c = getopt_long(argc, argv, "hf:s:t:ceugmpV", long_opts, NULL)) != -1)
{
switch (c)
{

View File

@@ -0,0 +1,71 @@
cmake_minimum_required(VERSION 3.29)
project(dyna LANGUAGES C CXX)
option(TRACY_ENABLE "Enable Tracy" ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_COLOR_DIAGNOSTICS ON)
include(cmake/CPM.cmake)
if(TRACY_ENABLE)
if(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
add_compile_options(-fno-omit-frame-pointer)
else()
message(WARNING "Tracy builds should be RelWithDebInfo!")
endif()
endif()
CPMAddPackage(
NAME glad
VERSION 2.0.8
GIT_REPOSITORY https://github.com/Dav1dde/glad.git
GIT_TAG glad2
)
add_subdirectory(${glad_SOURCE_DIR}/cmake ${CMAKE_CURRENT_BINARY_DIR}/glad)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../.. client/)
glad_add_library(glad_gl_core_33 STATIC API gl:core=3.3)
find_package(SDL3 REQUIRED)
find_package(SDL3_image REQUIRED)
add_executable(dyna
src/main.cpp
src/datapath.cpp
src/timer.cpp
src/gfx.cpp
src/texture.cpp
src/entity.cpp
src/world.cpp
src/map.cpp
src/player.cpp
src/monster.cpp
src/bomb.cpp
src/bonus.cpp
src/game.cpp
)
target_link_libraries(dyna
PRIVATE
glad_gl_core_33
SDL3::SDL3
SDL3_image::SDL3_image
Tracy::TracyClient
)
target_include_directories(dyna PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
# Mirror the data/ tree next to the executable so the game finds its assets
# when launched from the build directory (paths are resolved via SDL_GetBasePath).
add_custom_command(TARGET dyna POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/data
$<TARGET_FILE_DIR:dyna>/data
COMMENT "Copying data/ next to dyna executable"
)
file(GENERATE OUTPUT .gitignore CONTENT "*")

7
examples/dyna/LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Dyna.net copyright 2005 by Bartosz Taudul and Ralf Wrześniewski.
This program (including source code and the asset it uses) is NOT licensed
for any use other than being an example of how to integrate Tracy Profiler.
The license terms written in other parts of this repository DO NOT apply
here.

View File

@@ -0,0 +1,24 @@
# SPDX-License-Identifier: MIT
#
# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors
set(CPM_DOWNLOAD_VERSION 0.42.3)
set(CPM_HASH_SUM "a609e875fd532b067174250f6abbc3dac22fe2d64869783fb1e80bda1625c844")
if(CPM_SOURCE_CACHE)
set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
elseif(DEFINED ENV{CPM_SOURCE_CACHE})
set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
else()
set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake")
endif()
# Expand relative path. This is important if the provided path contains a tilde (~)
get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE)
file(DOWNLOAD
https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake
${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM}
)
include(${CPM_DOWNLOAD_LOCATION})

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

View File

@@ -0,0 +1,12 @@
10 1 0 0
@............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............

View File

@@ -0,0 +1,12 @@
20 4 0 0
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#@#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............

View File

@@ -0,0 +1,12 @@
40 3 2 0
@............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............

View File

@@ -0,0 +1,12 @@
40 3 3 0
@............
.###.#.#.###.
.............
.#.#.#.#.#.#.
.............
.###.#.#.###.
.............
.#.#.#.#.#.#.
.............
.###.#.#.###.
.............

View File

@@ -0,0 +1,12 @@
40 2 4 1
@............
.###.#.#.###.
.#.........#.
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.#.........#.
.###.#.#.###.
.............

View File

@@ -0,0 +1,12 @@
50 2 2 3
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#.
.....#.#.....
.#.###.###.#.
.............
.#.###.###.#.
.....#.#.....
.#.#.#.#.#.#.
.............
.#.#.#.#.#.#@

View File

@@ -0,0 +1,12 @@
60 3 3 3
@............
.#.#.###.#.#.
.............
.###.#.#.###.
.............
.#.#.###.#.#.
.............
.###.#.#.###.
.............
.#.#.###.#.#.
.............

View File

@@ -0,0 +1,12 @@
60 5 3 3
@............
.#.#.#.#.#.#.
.............
.#...#.#...#.
.............
.#.#.#.#.#.#.
.............
.#...#.#...#.
.............
.#.#.#.#.#.#.
.............

View File

@@ -0,0 +1,12 @@
90 5 5 5
@............
.#.........#.
.............
.............
.............
.............
.............
.............
.............
.#.........#.
.............

View File

@@ -0,0 +1,12 @@
30 4 4 4
@............
.#.#.#.#.#.#.
.............
.#...#.#...#.
.............
.#.#.#.#.#.#.
.............
.#...#.#...#.
.............
.#.#.#.#.#.#.
.............

143
examples/dyna/src/bomb.cpp Normal file
View File

@@ -0,0 +1,143 @@
#include "bomb.hpp"
#include "gfx.hpp"
#include "map.hpp"
#include "texture.hpp"
#include "timer.hpp"
#include "world.hpp"
#include <tracy/Tracy.hpp>
namespace dyna
{
Bomb::Bomb( int x_, int y_ )
: x( x_ )
, y( y_ )
, left( 9 )
{
}
void Bomb::draw()
{
ZoneScoped;
if( stage == Stage::exploding )
return;
if( stage == Stage::appear )
{
Textures::bomb_appear.bind( 9 - left );
}
else
{
int frame = static_cast<int>( ( time - left ) / static_cast<float>( time ) * 8 );
if( Timer::get_timestamp() / 100 % 2 == 0 )
frame++;
Textures::bomb.bind( frame );
}
Gfx::draw_square( x, y );
}
void Bomb::tick( World& world )
{
ZoneScoped;
delta += Timer::delta;
while( delta > 10 )
{
delta -= 10;
if( stage == Stage::appear )
{
if( left > 0 )
{
delta -= 10; // the fade-in advances at double speed
left--;
}
else
{
stage = Stage::ticking;
left = time;
}
}
else if( left > 0 )
{
left--;
}
else if( stage == Stage::ticking )
{
explode( world );
}
else
{
die( world );
}
}
}
void Bomb::explode( World& world )
{
ZoneScoped;
stage = Stage::exploding;
left = 200;
Map& map = world.map();
map.at( x, y ) = Field::explosion( Field::ExplosionType::center );
struct Dir
{
int dx, dy;
Field::ExplosionType through, tip;
};
const Dir dirs[4] = {
{ -1, 0, Field::ExplosionType::horizontal, Field::ExplosionType::left },
{ 1, 0, Field::ExplosionType::horizontal, Field::ExplosionType::right },
{ 0, -1, Field::ExplosionType::vertical, Field::ExplosionType::up },
{ 0, 1, Field::ExplosionType::vertical, Field::ExplosionType::down },
};
for( const Dir& d : dirs )
{
for( int i = 1; i <= maxrange; i++ )
{
int tx = x + d.dx * i;
int ty = y + d.dy * i;
if( tx < 0 || tx > map.getx() - 1 || ty < 0 || ty > map.gety() - 1 )
break;
Destruction destr = map.at( tx, ty ).destructible();
if( destr == Destruction::none )
break;
etiles.emplace_back( tx, ty );
if( map.at( tx, ty ).kind == Field::Kind::crate )
world.crates_left--;
if( i == maxrange || destr == Destruction::single )
{
map.at( tx, ty ) = Field::explosion( d.tip );
break;
}
else
{
map.at( tx, ty ) = Field::explosion( d.through );
}
}
}
}
void Bomb::die( World& world )
{
ZoneScoped;
dead = true;
Map& map = world.map();
map.at( x, y ) = Field::floor();
for( const auto& [tx, ty] : etiles )
map.at( tx, ty ) = Field::floor();
}
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <utility>
#include <vector>
namespace dyna
{
class World;
// A bomb on the grid: fades in, counts down, then paints a cross-shaped
// explosion onto the map and clears it again. Ported from bomb.cs.
class Bomb
{
public:
Bomb( int x, int y );
void draw();
void tick( World& world );
bool is_dead() const { return dead; }
private:
void explode( World& world );
void die( World& world );
enum class Stage
{
appear,
ticking,
exploding
};
int x, y; // grid coordinates
Stage stage = Stage::appear;
int left;
int delta = 0;
static constexpr int time = 150;
static constexpr int maxrange = 1;
std::vector<std::pair<int, int>> etiles; // tiles to revert to floor
bool dead = false;
};
}

View File

@@ -0,0 +1,58 @@
#include "bonus.hpp"
#include "gfx.hpp"
#include "texture.hpp"
#include "timer.hpp"
#include <tracy/Tracy.hpp>
namespace dyna
{
Vortex::Vortex( int gx, int gy )
{
x = gx; // stored in grid units, drawn via draw_square
y = gy;
set_action( Action::appear );
left = 79;
}
void Vortex::draw()
{
ZoneScoped;
int frame = static_cast<int>( ( Timer::get_timestamp() - action_start ) / 40 );
switch( action )
{
case Action::appear:
Textures::vortex_appear.bind( frame );
break;
case Action::wait:
Textures::vortex.bind( frame );
break;
default:
break;
}
Gfx::draw_square( x, y );
}
void Vortex::tick( World& )
{
ZoneScoped;
delta += Timer::delta;
while( delta > 10 )
{
delta -= 10;
if( left > 0 )
left--;
else if( action == Action::appear )
set_action( Action::wait );
}
}
void Vortex::die( World& ) {}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "entity.hpp"
namespace dyna
{
// The level-exit portal. Unlike the other entities its coordinates are stored in
// grid units (it draws via draw_square), matching bonus.cs.
class Vortex : public Entity
{
public:
Vortex( int gx, int gy );
void draw() override;
void tick( World& world ) override;
void die( World& world ) override;
};
}

View File

@@ -0,0 +1,25 @@
#include "datapath.hpp"
#include <SDL3/SDL.h>
#include <tracy/Tracy.hpp>
namespace dyna
{
std::string data_path( const std::string& rel )
{
ZoneScoped;
ZoneText( rel.c_str(), rel.size() );
// SDL_GetBasePath returns the executable's directory (with a trailing
// separator) and is owned by SDL, so cache it for the program's lifetime.
static const std::string base = []
{
const char* p = SDL_GetBasePath();
return std::string( p ? p : "" );
}();
return base + rel;
}
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include <string>
namespace dyna
{
// Resolve a path relative to the directory containing the executable, so the
// game finds its data files regardless of the current working directory (e.g.
// when launched from the build tree). The data/ tree is copied next to the
// binary at build time; see CMakeLists.txt.
std::string data_path( const std::string& rel );
}

View File

@@ -0,0 +1,39 @@
#include "entity.hpp"
#include "map.hpp"
#include "timer.hpp"
namespace dyna
{
void Entity::set_action( Action a )
{
action = a;
action_start = Timer::get_timestamp();
}
bool Entity::can_move( Action a, const Map& map ) const
{
switch( a )
{
case Action::up:
return y > 0 && !map.at( x / 64, y / 64 - 1 ).solid();
case Action::down:
return y / 64 < map.gety() - 1 && !map.at( x / 64, y / 64 + 1 ).solid();
case Action::left:
return x > 0 && !map.at( x / 64 - 1, y / 64 ).solid();
case Action::right:
return x / 64 < map.getx() - 1 && !map.at( x / 64 + 1, y / 64 ).solid();
default:
return true;
}
}
bool Entity::killed( const Map& map ) const
{
int tx = ( x + 32 ) / 64;
int ty = ( y + 32 ) / 64;
return map.at( tx, ty ).kind == Field::Kind::explosion;
}
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <cstdint>
namespace dyna
{
class Map;
class World;
// Movement/state verbs shared by the player and monsters. In the C# source this
// lived as Entity.Action; promoted to namespace scope so Game can refer to it.
enum class Action
{
wait,
up,
down,
left,
right,
death,
place_bomb,
appear
};
// Base for everything that moves on the grid. Coordinates are in pixels
// (64 per tile) and laid out top-left origin, matching entity.cs.
class Entity
{
public:
virtual ~Entity() = default;
virtual void set_action( Action a );
int getx() const { return x; }
int gety() const { return y; }
virtual void draw() = 0;
virtual void tick( World& world ) = 0;
virtual void die( World& world ) = 0;
protected:
bool can_move( Action a, const Map& map ) const;
virtual bool killed( const Map& map ) const;
int x = 0, y = 0;
std::int64_t action_start = 0;
int delta = 0;
Action action = Action::wait;
int left = 0;
};
}

211
examples/dyna/src/game.cpp Normal file
View File

@@ -0,0 +1,211 @@
#include "game.hpp"
#include "datapath.hpp"
#include "gfx.hpp"
#include "map.hpp"
#include "player.hpp"
#include "timer.hpp"
#include "world.hpp"
#include <SDL3/SDL.h>
#include <tracy/Tracy.hpp>
#include <string>
namespace dyna
{
namespace Game
{
namespace
{
struct TracySection
{
explicit TracySection( const char* name ) { Enter( name ); }
explicit TracySection( const char* name, uint16_t category ) : idx( TracySectionEnterCategory( category, "%s", name ) ) {}
~TracySection() { Leave(); }
void Enter( const char* name )
{
idx = TracySectionEnter( "%s", name );
}
void Leave()
{
if( idx > 0 )
{
TracySectionLeave( idx );
idx = 0;
}
}
private:
uint32_t idx;
};
SDL_Keycode key = 0; // most recently pressed movement key
bool help = false;
// Run one level to completion. Returns true if the player asked to quit the
// whole application (window close), false if the level simply ended (death,
// escape, or reaching the exit) and control should return to the caller.
bool level_loop( World& world )
{
TracySection section( ( std::string( "Level " ) + world.name() ).c_str(), 1 );
Player* p = world.player();
for( ;; )
{
SDL_Event ev;
while( SDL_PollEvent( &ev ) )
{
if( ev.type == SDL_EVENT_QUIT )
return true;
if( ev.type == SDL_EVENT_KEY_DOWN && !ev.key.repeat )
{
switch( ev.key.key )
{
case SDLK_ESCAPE:
world.killed = true;
return false;
case SDLK_LEFT:
key = SDLK_LEFT;
p->move( Action::left );
break;
case SDLK_RIGHT:
key = SDLK_RIGHT;
p->move( Action::right );
break;
case SDLK_UP:
key = SDLK_UP;
p->move( Action::up );
break;
case SDLK_DOWN:
key = SDLK_DOWN;
p->move( Action::down );
break;
case SDLK_SPACE:
world.map().place_bomb( ( p->getx() + 32 ) / 64, ( p->gety() + 32 ) / 64 );
break;
default:
break;
}
}
if( ev.type == SDL_EVENT_KEY_UP )
{
switch( ev.key.key )
{
case SDLK_LEFT:
if( key == SDLK_LEFT ) p->move( Action::wait );
break;
case SDLK_RIGHT:
if( key == SDLK_RIGHT ) p->move( Action::wait );
break;
case SDLK_UP:
if( key == SDLK_UP ) p->move( Action::wait );
break;
case SDLK_DOWN:
if( key == SDLK_DOWN ) p->move( Action::wait );
break;
default:
break;
}
}
}
Gfx::clear();
Timer::tick();
world.tick();
world.draw();
Gfx::swap();
if( world.killed || world.next_level )
return false;
}
}
// Play through the levels in order. Returns true if the application should quit.
bool new_game()
{
TracySection section( "In-game" );
int level = 1;
for( ;; )
{
World world( data_path( "data/levels/" + std::to_string( level ) ), true );
if( level_loop( world ) )
return true; // window closed
if( world.killed )
return false; // died or escaped to the menu
if( ++level >= 10 )
return false; // cleared the last level
}
}
} // namespace
void menu_loop()
{
constexpr const char* sectionName = "Main menu";
TracySection section( sectionName );
World world( data_path( "data/levels/menu" ), false );
for( ;; )
{
SDL_Event ev;
while( SDL_PollEvent( &ev ) )
{
if( ev.type == SDL_EVENT_QUIT )
return;
if( ev.type == SDL_EVENT_KEY_DOWN && !ev.key.repeat )
{
switch( ev.key.key )
{
case SDLK_ESCAPE:
return;
case SDLK_SPACE:
section.Leave();
if( new_game() )
return; // window closed during play
section.Enter( sectionName );
break;
case SDLK_H:
help = !help;
break;
default:
break;
}
}
}
Gfx::clear();
Timer::tick();
world.tick();
world.draw();
if( help )
Gfx::show_help();
else
Gfx::show_menu();
Gfx::swap();
}
}
} // namespace Game
}

View File

@@ -0,0 +1,14 @@
#pragma once
namespace dyna
{
// Top-level game flow, ported from game.cs. The C# original kept the running
// game's state (player, map, win/lose flags) in static fields; that state now
// lives in a World object owned by the loops below, so nothing leaks out here.
namespace Game
{
void menu_loop();
}
}

517
examples/dyna/src/gfx.cpp Normal file
View File

@@ -0,0 +1,517 @@
#include "gfx.hpp"
#include "texture.hpp"
#include "timer.hpp"
#include <SDL3/SDL.h>
#include <tracy/Tracy.hpp>
#include <cassert>
#include <cstdio>
#include <vector>
namespace dyna
{
namespace
{
SDL_Window* g_window = nullptr;
SDL_GLContext g_gl_context = nullptr;
GLuint g_program = 0;
GLuint g_vao = 0;
GLuint g_vbo = 0;
// Current draw state, applied to every quad appended to the batch.
GLuint g_current_tex = 0;
int g_current_layer = 0;
float g_alpha = 1.0f;
// One vertex of the streaming batch: screen position, atlas-array texcoord,
// the array layer to sample and a per-vertex alpha multiplier.
struct GlVert
{
float px, py, tx, ty, layer, a;
};
// A run of consecutive vertices that share one texture, drawn in a single call.
struct DrawCmd
{
GLuint tex;
GLsizei count;
};
std::vector<GlVert> g_verts;
std::vector<DrawCmd> g_cmds;
const char* VERT_SRC = R"(
#version 330 core
uniform mat4 uProjection;
layout(location = 0) in vec2 aPosition;
layout(location = 1) in vec2 aTexCoord;
layout(location = 2) in float aLayer;
layout(location = 3) in float aAlpha;
out vec3 vTexCoord;
out float vAlpha;
void main() {
gl_Position = uProjection * vec4(aPosition, 0.0, 1.0);
vTexCoord = vec3(aTexCoord, aLayer);
vAlpha = aAlpha;
}
)";
const char* FRAG_SRC = R"(
#version 330 core
uniform sampler2DArray uTexture;
in vec3 vTexCoord;
in float vAlpha;
out vec4 fragColor;
void main() {
fragColor = texture(uTexture, vTexCoord) * vec4(1.0, 1.0, 1.0, vAlpha);
}
)";
GLuint compile_shader( GLenum type, const char* src )
{
ZoneScoped;
GLuint s = glCreateShader( type );
glShaderSource( s, 1, &src, nullptr );
glCompileShader( s );
GLint ok = 0;
glGetShaderiv( s, GL_COMPILE_STATUS, &ok );
if( !ok )
{
char log[512];
glGetShaderInfoLog( s, 512, nullptr, log );
std::fprintf( stderr, "Shader compile error: %s\n", log );
glDeleteShader( s );
return 0;
}
return s;
}
bool init_shaders()
{
ZoneScoped;
GLuint vs = compile_shader( GL_VERTEX_SHADER, VERT_SRC );
if( !vs ) return false;
GLuint fs = compile_shader( GL_FRAGMENT_SHADER, FRAG_SRC );
if( !fs )
{
glDeleteShader( vs );
return false;
}
g_program = glCreateProgram();
glAttachShader( g_program, vs );
glAttachShader( g_program, fs );
glLinkProgram( g_program );
glDeleteShader( vs );
glDeleteShader( fs );
GLint ok = 0;
glGetProgramiv( g_program, GL_LINK_STATUS, &ok );
if( !ok )
{
char log[512];
glGetProgramInfoLog( g_program, 512, nullptr, log );
std::fprintf( stderr, "Program link error: %s\n", log );
glDeleteProgram( g_program );
g_program = 0;
return false;
}
// Bottom-left origin orthographic projection, matching the original
// gluOrtho2D(0, w, 0, h) so the ported draw code carries over verbatim.
float l = 0.0f, r = static_cast<float>( Gfx::w );
float b = 0.0f, t = static_cast<float>( Gfx::h );
float proj[16] = {
2.0f / ( r - l ), 0.0f, 0.0f, 0.0f,
0.0f, 2.0f / ( t - b ), 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, 0.0f,
-( r + l ) / ( r - l ), -( t + b ) / ( t - b ), 0.0f, 1.0f };
glUseProgram( g_program );
glUniformMatrix4fv( glGetUniformLocation( g_program, "uProjection" ), 1, GL_FALSE, proj );
glUniform1i( glGetUniformLocation( g_program, "uTexture" ), 0 );
glUseProgram( 0 );
return true;
}
void init_quad_vao()
{
ZoneScoped;
glGenVertexArrays( 1, &g_vao );
glGenBuffers( 1, &g_vbo );
glBindVertexArray( g_vao );
glBindBuffer( GL_ARRAY_BUFFER, g_vbo );
const GLsizei stride = sizeof( GlVert );
glEnableVertexAttribArray( 0 );
glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, stride, (void*)0 );
glEnableVertexAttribArray( 1 );
glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, stride, (void*)8 );
glEnableVertexAttribArray( 2 );
glVertexAttribPointer( 2, 1, GL_FLOAT, GL_FALSE, stride, (void*)16 );
glEnableVertexAttribArray( 3 );
glVertexAttribPointer( 3, 1, GL_FLOAT, GL_FALSE, stride, (void*)20 );
glBindVertexArray( 0 );
glBindBuffer( GL_ARRAY_BUFFER, 0 );
}
// Draw and clear everything accumulated since the last flush, in submission
// order. Consecutive quads that share a texture collapse into one draw call.
void flush_batch()
{
ZoneScoped;
if( g_verts.empty() )
return;
glBindBuffer( GL_ARRAY_BUFFER, g_vbo );
glBufferData( GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>( g_verts.size() * sizeof( GlVert ) ),
g_verts.data(), GL_STREAM_DRAW );
glUseProgram( g_program );
glBindVertexArray( g_vao );
GLint offset = 0;
for( const DrawCmd& cmd : g_cmds )
{
glBindTexture( GL_TEXTURE_2D_ARRAY, cmd.tex );
glDrawArrays( GL_TRIANGLES, offset, cmd.count );
offset += cmd.count;
}
glBindVertexArray( 0 );
glUseProgram( 0 );
glBindBuffer( GL_ARRAY_BUFFER, 0 );
g_verts.clear();
g_cmds.clear();
}
// Frame image capture, following the OpenGL example in the Tracy manual. The
// backbuffer is downscaled on the GPU to a small fixed size and read back
// asynchronously, so a screenshot can be attached to every frame without
// stalling the CPU on the GPU. Several buffer sets are cycled because rendering
// runs a few frames ahead of the GPU.
// Half the render resolution, preserving its aspect ratio; both dimensions
// stay divisible by 4 as FrameImage requires.
constexpr int FI_W = Gfx::w / 2;
constexpr int FI_H = Gfx::h / 2;
constexpr int FI_COUNT = 4;
GLuint g_fi_texture[FI_COUNT];
GLuint g_fi_framebuffer[FI_COUNT];
GLuint g_fi_pbo[FI_COUNT];
GLsync g_fi_fence[FI_COUNT] = {};
int g_fi_idx = 0;
std::vector<int> g_fi_queue;
void init_frame_images()
{
ZoneScoped;
glGenTextures( FI_COUNT, g_fi_texture );
glGenFramebuffers( FI_COUNT, g_fi_framebuffer );
glGenBuffers( FI_COUNT, g_fi_pbo );
for( int i = 0; i < FI_COUNT; i++ )
{
glBindTexture( GL_TEXTURE_2D, g_fi_texture[i] );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, FI_W, FI_H, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr );
glBindFramebuffer( GL_FRAMEBUFFER, g_fi_framebuffer[i] );
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_fi_texture[i], 0 );
glBindBuffer( GL_PIXEL_PACK_BUFFER, g_fi_pbo[i] );
glBufferData( GL_PIXEL_PACK_BUFFER, FI_W * FI_H * 4, nullptr, GL_STREAM_READ );
}
glBindFramebuffer( GL_FRAMEBUFFER, 0 );
glBindBuffer( GL_PIXEL_PACK_BUFFER, 0 );
}
void shutdown_frame_images()
{
ZoneScoped;
glDeleteTextures( FI_COUNT, g_fi_texture );
glDeleteFramebuffers( FI_COUNT, g_fi_framebuffer );
glDeleteBuffers( FI_COUNT, g_fi_pbo );
}
// Send any captures the GPU has already finished, then queue a capture of the
// frame just rendered. Call after the batch is flushed but before swapping.
void capture_frame_image()
{
ZoneScoped;
// Hand finished captures from earlier frames to the profiler. The queue
// size is the number of frames we are still ahead of the GPU, which is the
// frame lag Tracy needs as the FrameImage offset.
while( !g_fi_queue.empty() )
{
const int idx = g_fi_queue.front();
if( glClientWaitSync( g_fi_fence[idx], 0, 0 ) == GL_TIMEOUT_EXPIRED ) break;
glDeleteSync( g_fi_fence[idx] );
glBindBuffer( GL_PIXEL_PACK_BUFFER, g_fi_pbo[idx] );
void* ptr = glMapBufferRange( GL_PIXEL_PACK_BUFFER, 0, FI_W * FI_H * 4, GL_MAP_READ_BIT );
FrameImage( ptr, FI_W, FI_H, g_fi_queue.size(), true );
glUnmapBuffer( GL_PIXEL_PACK_BUFFER );
g_fi_queue.erase( g_fi_queue.begin() );
}
// Downscale the current backbuffer into the next buffer set and start an
// asynchronous read-back, signalled by a fence.
assert( g_fi_queue.empty() || g_fi_queue.front() != g_fi_idx ); // buffer overrun
glBindFramebuffer( GL_DRAW_FRAMEBUFFER, g_fi_framebuffer[g_fi_idx] );
glBlitFramebuffer( 0, 0, Gfx::w, Gfx::h, 0, 0, FI_W, FI_H, GL_COLOR_BUFFER_BIT, GL_LINEAR );
glBindFramebuffer( GL_DRAW_FRAMEBUFFER, 0 );
glBindFramebuffer( GL_READ_FRAMEBUFFER, g_fi_framebuffer[g_fi_idx] );
glBindBuffer( GL_PIXEL_PACK_BUFFER, g_fi_pbo[g_fi_idx] );
glReadPixels( 0, 0, FI_W, FI_H, GL_RGBA, GL_UNSIGNED_BYTE, nullptr );
glBindFramebuffer( GL_READ_FRAMEBUFFER, 0 );
g_fi_fence[g_fi_idx] = glFenceSync( GL_SYNC_GPU_COMMANDS_COMPLETE, 0 );
g_fi_queue.emplace_back( g_fi_idx );
g_fi_idx = ( g_fi_idx + 1 ) % FI_COUNT;
}
} // namespace
namespace Render
{
bool init()
{
ZoneScoped;
if( !init_shaders() ) return false;
init_quad_vao();
init_frame_images();
return true;
}
void shutdown()
{
ZoneScoped;
shutdown_frame_images();
if( g_vbo ) glDeleteBuffers( 1, &g_vbo );
if( g_vao ) glDeleteVertexArrays( 1, &g_vao );
if( g_program ) glDeleteProgram( g_program );
g_vbo = g_vao = g_program = 0;
}
void use_texture( GLuint tex, int layer )
{
g_current_tex = tex;
g_current_layer = layer;
}
GLuint make_texture( int w, int h, int layers, const void* rgba )
{
ZoneScoped;
GLuint tex = 0;
glGenTextures( 1, &tex );
glBindTexture( GL_TEXTURE_2D_ARRAY, tex );
glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
glPixelStorei( GL_UNPACK_ALIGNMENT, 1 );
glTexImage3D( GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, w, h, layers, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba );
return tex;
}
} // namespace Render
namespace Gfx
{
void clear()
{
glClear( GL_COLOR_BUFFER_BIT );
}
void swap()
{
ZoneScoped;
flush_batch();
capture_frame_image();
SDL_GL_SwapWindow( g_window );
FrameMark;
}
void alpha( float a )
{
g_alpha = a;
}
void draw_quad( const Vertex corners[4] )
{
ZoneScoped;
// Two triangles, vertices appended in submission order so painter ordering
// (and the transient per-monster alpha) is preserved by the batch.
const int idx[6] = { 0, 1, 2, 0, 2, 3 };
for( int i : idx )
{
const Vertex& c = corners[i];
g_verts.push_back( { c.x, c.y, c.u, c.v,
static_cast<float>( g_current_layer ), g_alpha } );
}
if( !g_cmds.empty() && g_cmds.back().tex == g_current_tex )
g_cmds.back().count += 6;
else
g_cmds.push_back( { g_current_tex, 6 } );
}
void draw_sprite( int x, int y )
{
ZoneScoped;
float fx = static_cast<float>( x );
float fy = static_cast<float>( y );
float top = static_cast<float>( h ) - fy;
float bottom = static_cast<float>( h ) - ( fy + 64.0f );
Vertex corners[4] = {
{ fx, top, 0.0f, 0.0f },
{ fx + 64.0f, top, 1.0f, 0.0f },
{ fx + 64.0f, bottom, 1.0f, 1.0f },
{ fx, bottom, 0.0f, 1.0f },
};
draw_quad( corners );
}
void draw_square( int x, int y )
{
draw_sprite( x * 64, y * 64 );
}
void show_help()
{
ZoneScoped;
Textures::menu.bind();
const float fw = static_cast<float>( w );
const float fh = static_cast<float>( h );
Vertex bg[4] = {
{ 0.0f, fh, 0.0f, 0.0f },
{ fw, fh, 832.0f / 1024, 0.0f },
{ fw, 0.0f, 832.0f / 1024, 704.0f / 1024 },
{ 0.0f, 0.0f, 0.0f, 704.0f / 1024 },
};
draw_quad( bg );
int t = static_cast<int>( Timer::get_timestamp() / 40 );
Textures::p_r.bind( t );
draw_sprite( 150, 85 );
Textures::m1_r.bind( t );
draw_sprite( 75, 160 );
Textures::m2_r.bind( t );
draw_sprite( 150, 160 );
Textures::m3_r.bind( t );
draw_sprite( 225, 160 );
Textures::bomb.bind( static_cast<int>( Timer::get_timestamp() / 100 % 2 ) );
draw_sprite( 150, 235 );
Textures::wall.bind();
draw_sprite( 150, 310 );
Textures::crate.bind();
draw_sprite( 150, 385 );
Textures::vortex.bind( t );
draw_sprite( 150, 460 );
Textures::bonus1.bind( t );
draw_sprite( 112, 535 );
Textures::bonus2.bind( t );
draw_sprite( 187, 535 );
}
void show_menu()
{
ZoneScoped;
Textures::menu.bind();
Vertex logo[4] = {
{ float( ( w - 594 ) / 2 ), float( h - 50 ), 1.0f, 0.0f },
{ float( ( w + 594 ) / 2 ), float( h - 50 ), 1.0f, 594.0f / 1024 },
{ float( ( w + 594 ) / 2 ), float( h - 50 - 180 ), 1.0f - 180.0f / 1024, 594.0f / 1024 },
{ float( ( w - 594 ) / 2 ), float( h - 50 - 180 ), 1.0f - 180.0f / 1024, 0.0f },
};
draw_quad( logo );
Vertex prompt[4] = {
{ float( ( w - 527 ) / 2 ), 335.0f, 0.0f, 704.0f / 1024 },
{ float( ( w + 527 ) / 2 ), 335.0f, 527.0f / 1024, 704.0f / 1024 },
{ float( ( w + 527 ) / 2 ), 20.0f, 527.0f / 1024, 1019.0f / 1024 },
{ float( ( w - 527 ) / 2 ), 20.0f, 0.0f, 1019.0f / 1024 },
};
draw_quad( prompt );
}
} // namespace Gfx
namespace Init
{
bool all()
{
ZoneScoped;
if( !SDL_Init( SDL_INIT_VIDEO ) )
{
std::fprintf( stderr, "SDL_Init failed: %s\n", SDL_GetError() );
return false;
}
SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );
SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3 );
SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 3 );
SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE );
g_window = SDL_CreateWindow( "Dyna.net", Gfx::w, Gfx::h, SDL_WINDOW_OPENGL );
if( !g_window )
{
std::fprintf( stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError() );
return false;
}
g_gl_context = SDL_GL_CreateContext( g_window );
if( !g_gl_context )
{
std::fprintf( stderr, "SDL_GL_CreateContext failed: %s\n", SDL_GetError() );
return false;
}
int version = gladLoadGL( (GLADloadfunc)SDL_GL_GetProcAddress );
if( version == 0 )
{
std::fprintf( stderr, "gladLoadGL failed\n" );
return false;
}
SDL_GL_SetSwapInterval( 1 ); // vsync; the game is time-based so speed is unaffected
glViewport( 0, 0, Gfx::w, Gfx::h );
glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
if( !Render::init() ) return false;
Timer::reset();
Textures::preload();
return true;
}
void shutdown()
{
ZoneScoped;
Render::shutdown();
if( g_gl_context ) SDL_GL_DestroyContext( g_gl_context );
if( g_window ) SDL_DestroyWindow( g_window );
SDL_Quit();
}
} // namespace Init
}

59
examples/dyna/src/gfx.hpp Normal file
View File

@@ -0,0 +1,59 @@
#pragma once
#include <glad/gl.h>
namespace dyna
{
// Screen dimensions, matching the original 13x11 grid of 64px tiles.
namespace Gfx
{
constexpr int w = 832;
constexpr int h = 704;
void clear();
void swap();
// Drawing primitives ported from gfx.cs. They render with the currently
// bound texture (see Texture::bind) and the current alpha. The coordinate
// system is bottom-left origin with y growing upward, exactly as the C#
// gluOrtho2D setup; draw_sprite/draw_square take y measured from the top
// and flip internally, so game-side coordinates stay top-left based.
void alpha( float a );
void draw_sprite( int x, int y ); // pixel position of the top-left corner
void draw_square( int x, int y ); // grid position (multiplied by 64)
// A single textured quad given four explicit (position, texcoord) corners,
// used by the menu/help screens which sample rotated regions of the atlas.
struct Vertex
{
float x, y, u, v;
};
void draw_quad( const Vertex corners[4] );
void show_help();
void show_menu();
}
// Renderer back end shared by the texture loaders.
namespace Render
{
bool init(); // shaders + streaming VBO/VAO
void shutdown(); // delete the program and buffers
// Select the array texture (and layer within it) used by subsequent draws.
void use_texture( GLuint tex, int layer );
// Upload `layers` tightly packed RGBA8 images of size w*h as one
// GL_TEXTURE_2D_ARRAY and return its name (0 on failure).
GLuint make_texture( int w, int h, int layers, const void* rgba );
}
// One-time startup/shutdown, ported from the Init class in gfx.cs.
namespace Init
{
bool all(); // SDL, GL context, renderer, textures, timer
void shutdown();
}
}

View File

@@ -0,0 +1,41 @@
#include "game.hpp"
#include "gfx.hpp"
#include <SDL3/SDL_main.h>
#include <tracy/Tracy.hpp>
#include <cstdlib>
#include <new>
// Route every heap allocation through Tracy so the profiler can track memory
// usage. The default array forms (operator new[]/delete[]) and the nothrow
// forms forward to these, so overriding the scalar operators covers them too.
void* operator new( std::size_t count )
{
void* ptr = std::malloc( count );
if( !ptr ) throw std::bad_alloc();
TracyAlloc( ptr, count );
return ptr;
}
void operator delete( void* ptr ) noexcept
{
TracyFree( ptr );
std::free( ptr );
}
int main( int /*argc*/, char* /*argv*/[] )
{
TracyNoop;
TracySectionSetup( 0, "Game state" );
TracySectionSetup( 1, "Level progression" );
if( !dyna::Init::all() )
return 1;
dyna::Game::menu_loop();
dyna::Init::shutdown();
return 0;
}

334
examples/dyna/src/map.cpp Normal file
View File

@@ -0,0 +1,334 @@
#include "map.hpp"
#include "bomb.hpp"
#include "bonus.hpp"
#include "gfx.hpp"
#include "monster.hpp"
#include "player.hpp"
#include "texture.hpp"
#include "timer.hpp"
#include "world.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <fstream>
#include <sstream>
#include <tracy/Tracy.hpp>
namespace dyna
{
// ---- Field --------------------------------------------------------------
Field Field::explosion( ExplosionType t )
{
Field f;
f.kind = Kind::explosion;
f.etype = t;
f.tstart = Timer::get_timestamp();
return f;
}
bool Field::solid() const
{
switch( kind )
{
case Kind::wall:
case Kind::crate:
case Kind::bomb:
return true;
default:
return false;
}
}
Destruction Field::destructible() const
{
switch( kind )
{
case Kind::floor:
return Destruction::multi;
case Kind::crate:
return Destruction::single;
default:
return Destruction::none;
}
}
void Field::draw( int x, int y ) const
{
switch( kind )
{
case Kind::wall:
Textures::wall.bind();
Gfx::draw_square( x, y );
break;
case Kind::crate:
Textures::sand.bind();
Gfx::draw_square( x, y );
Textures::crate.bind();
Gfx::draw_square( x, y );
break;
case Kind::explosion: {
Textures::sand.bind();
Gfx::draw_square( x, y );
int frame = static_cast<int>( ( Timer::get_timestamp() - tstart ) / 40 % 8 );
if( frame > 4 ) frame = 8 - frame;
switch( etype )
{
case ExplosionType::center: Textures::e_c.bind( frame ); break;
case ExplosionType::vertical: Textures::e_v.bind( frame ); break;
case ExplosionType::horizontal: Textures::e_h.bind( frame ); break;
case ExplosionType::left: Textures::e_le.bind( frame ); break;
case ExplosionType::right: Textures::e_re.bind( frame ); break;
case ExplosionType::up: Textures::e_ue.bind( frame ); break;
case ExplosionType::down: Textures::e_de.bind( frame ); break;
}
Gfx::draw_square( x, y );
break;
}
// floor, bomb and vortex tiles all show plain sand; the bomb and vortex
// sprites themselves are drawn by their entities.
case Kind::floor:
case Kind::bomb:
case Kind::vortex:
default:
Textures::sand.bind();
Gfx::draw_square( x, y );
break;
}
}
// ---- Map ----------------------------------------------------------------
Map::Map( const std::string& fn )
{
ZoneScoped;
ZoneText( fn.c_str(), fn.size() );
load( fn );
generate_destructibles();
populate_map();
}
Map::~Map() = default;
void Map::load( const std::string& fn )
{
ZoneScoped;
std::ifstream f( fn );
if( !f )
{
std::fprintf( stderr, "Cannot open level %s\n", fn.c_str() );
grid.assign( X * Y, Field::floor() );
return;
}
std::stringstream buf;
buf << f.rdbuf();
std::string content = buf.str();
size_t nl = content.find( '\n' );
std::string header = ( nl == std::string::npos ) ? content : content.substr( 0, nl );
std::sscanf( header.c_str(), "%d %d %d %d", &destructibles, &m1, &m2, &m3 );
grid.assign( X * Y, Field::floor() );
px = -1;
size_t p = ( nl == std::string::npos ) ? content.size() : nl + 1;
for( int ry = 0; ry < Y; ry++ )
{
for( int rx = 0; rx < X; rx++ )
{
char c = ( p < content.size() ) ? content[p++] : '\0';
switch( c )
{
case '.':
at( rx, ry ) = Field::floor();
break;
case '#':
at( rx, ry ) = Field::wall();
break;
case '@':
at( rx, ry ) = Field::floor();
px = rx;
py = ry;
break;
case '\n':
rx--; // newlines don't consume a grid cell
break;
default:
break;
}
}
}
}
bool Map::monster_ok( int rx, int ry, int pxx, int pyy, int r ) const
{
const Field& f = at( rx, ry );
return f.is_floor_family() && f.kind != Field::Kind::crate &&
( std::abs( rx - pxx ) > r || std::abs( ry - pyy ) > r );
}
void Map::generate_destructibles()
{
ZoneScoped;
int i = destructibles;
while( i != 0 )
{
int rx = RNG::next( X );
int ry = RNG::next( Y );
if( monster_ok( rx, ry, px, py, 1 ) )
{
at( rx, ry ) = Field::crate();
i--;
}
}
}
void Map::populate_map()
{
ZoneScoped;
for( int type = 1; type <= 3; type++ )
{
int count = ( type == 1 ) ? m1 : ( type == 2 ) ? m2
: m3;
while( count != 0 )
{
int rx = RNG::next( X );
int ry = RNG::next( Y );
if( monster_ok( rx, ry, px, py, 2 ) )
{
monsters.push_back( std::make_unique<Monster>( type, rx, ry ) );
count--;
}
}
}
}
void Map::draw()
{
ZoneScoped;
for( int ry = 0; ry < Y; ry++ )
for( int rx = 0; rx < X; rx++ )
at( rx, ry ).draw( rx, ry );
for( auto& b : bombs ) b->draw();
for( auto& e : monsters ) e->draw();
for( auto& e : bonuses ) e->draw();
}
void Map::tick( World& world )
{
ZoneScoped;
// Bombs.
for( auto& b : bombs ) b->tick( world );
bombs.erase( std::remove_if( bombs.begin(), bombs.end(),
[]( const std::unique_ptr<Bomb>& b ) { return b->is_dead(); } ),
bombs.end() );
// Monsters: tick, then retire the dead and queue their respawn timers.
for( auto& e : monsters ) e->tick( world );
for( auto& e : monsters )
{
if( e->is_dead() )
{
int delay = ( e->type() == 1 ) ? 10000 : ( e->type() == 2 ) ? 20000
: 30000;
mwait.push_back( { e->type(), Timer::get_timestamp() + delay } );
}
}
monsters.erase( std::remove_if( monsters.begin(), monsters.end(),
[]( const std::unique_ptr<Monster>& e ) { return e->is_dead(); } ),
monsters.end() );
// The respawn and exit-portal placement below need the player's position;
// they only fire during gameplay (a monster died, or every crate is gone),
// never on the player-less menu screen.
Player* player = world.player();
// Respawn monsters whose wait has elapsed.
std::int64_t now = Timer::get_timestamp();
std::vector<MWait> still_waiting;
for( const MWait& m : mwait )
{
if( m.time < now && player )
{
int rx = 0, ry = 0;
bool ok = false;
while( !ok )
{
rx = RNG::next( X );
ry = RNG::next( Y );
if( monster_ok( rx, ry, player->getx() / 64, player->gety() / 64, 3 ) )
ok = true;
}
auto monster = std::make_unique<Monster>( m.type, rx, ry );
monster->set_action( Action::appear );
monsters.push_back( std::move( monster ) );
}
else
{
still_waiting.push_back( m );
}
}
mwait = std::move( still_waiting );
// Bonuses.
for( auto& e : bonuses ) e->tick( world );
// Once every crate is gone, open the exit portal somewhere clear.
if( world.crates_left == 0 && player )
{
world.crates_left--;
int rx = 0, ry = 0;
bool ok = false;
while( !ok )
{
rx = RNG::next( X );
ry = RNG::next( Y );
if( monster_ok( rx, ry, player->getx() / 64, player->gety() / 64, 4 ) )
ok = true;
}
at( rx, ry ) = Field::vortex();
bonuses.push_back( std::make_unique<Vortex>( rx, ry ) );
}
}
std::unique_ptr<Player> Map::create_player() const
{
return std::make_unique<Player>( px, py );
}
void Map::place_bomb( int x, int y )
{
Field& f = at( x, y );
if( f.is_floor_family() && f.kind != Field::Kind::bomb )
{
f = Field::bomb();
bombs.push_back( std::make_unique<Bomb>( x, y ) );
}
}
bool Map::monster_collide( int tx, int ty ) const
{
for( const auto& e : monsters )
{
if( ( e->getx() + 32 ) / 64 == ( tx + 32 ) / 64 &&
( e->gety() + 32 ) / 64 == ( ty + 32 ) / 64 )
return true;
}
return false;
}
}

120
examples/dyna/src/map.hpp Normal file
View File

@@ -0,0 +1,120 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace dyna
{
class Player;
class Bomb;
class Monster;
class Vortex;
class World;
// How a tile reacts to an explosion sweeping through it.
enum class Destruction
{
none, // blocks the blast (wall, bomb, existing explosion, vortex)
single, // destroyed and stops the blast (crate)
multi // passable, blast continues (floor)
};
// A grid cell. The C# version used a small class hierarchy rooted at a Field
// interface; since the variants differ only in a couple of flags and how they
// draw, this collapses them into one value type tagged by Kind. Note that in
// the original everything except Wall derived from Floor, so the "is Floor"
// checks there map to "kind != Wall" here.
struct Field
{
enum class Kind
{
floor,
wall,
crate,
bomb, // tile occupied by a live bomb (solid, indestructible)
explosion, // transient blast tile
vortex // level exit portal
};
enum class ExplosionType
{
center,
vertical,
horizontal,
left,
right,
down,
up
};
Kind kind = Kind::floor;
ExplosionType etype = ExplosionType::center;
std::int64_t tstart = 0; // explosion animation start, set on creation
static Field floor() { return Field{}; }
static Field wall() { return Field{ Kind::wall, {}, 0 }; }
static Field crate() { return Field{ Kind::crate, {}, 0 }; }
static Field bomb() { return Field{ Kind::bomb, {}, 0 }; }
static Field vortex() { return Field{ Kind::vortex, {}, 0 }; }
static Field explosion( ExplosionType t );
bool solid() const;
Destruction destructible() const;
void draw( int x, int y ) const;
bool is_floor_family() const { return kind != Kind::wall; }
};
class Map
{
public:
explicit Map( const std::string& fn );
~Map(); // defined in map.cpp where the entity types are complete
Field& at( int x, int y ) { return grid[index( x, y )]; }
const Field& at( int x, int y ) const { return grid[index( x, y )]; }
void draw();
void tick( World& world );
int getx() const { return X; }
int gety() const { return Y; }
int get_crates() const { return destructibles; }
std::unique_ptr<Player> create_player() const;
void place_bomb( int x, int y );
bool monster_collide( int tx, int ty ) const;
private:
static constexpr int X = 13, Y = 11;
// Deferred monster respawn timer, mirroring Map.MWait.
struct MWait
{
int type; // 1, 2 or 3
std::int64_t time; // timestamp at which it respawns
};
static int index( int x, int y ) { return x * Y + y; }
void load( const std::string& fn );
void generate_destructibles();
void populate_map();
bool monster_ok( int rx, int ry, int px, int py, int r ) const;
std::vector<Field> grid;
int px = -10, py = -10;
int destructibles = 0;
int m1 = 0, m2 = 0, m3 = 0;
std::vector<std::unique_ptr<Bomb>> bombs;
std::vector<std::unique_ptr<Monster>> monsters;
std::vector<std::unique_ptr<Vortex>> bonuses;
std::vector<MWait> mwait;
};
}

View File

@@ -0,0 +1,227 @@
#include "monster.hpp"
#include "gfx.hpp"
#include "map.hpp"
#include "texture.hpp"
#include "timer.hpp"
#include "world.hpp"
#include <tracy/Tracy.hpp>
namespace dyna
{
namespace
{
bool is_opposite( Action a, Action b )
{
return ( a == Action::up && b == Action::down ) ||
( a == Action::down && b == Action::up ) ||
( a == Action::left && b == Action::right ) ||
( a == Action::right && b == Action::left );
}
} // namespace
Monster::Monster( int type, int gx, int gy )
: mtype( type )
, t( type == 1 ? 14 : type == 2 ? 11
: 7 )
{
x = gx * 64;
y = gy * 64;
}
void Monster::set_action( Action a )
{
Entity::set_action( a );
if( action == Action::appear )
left = 200;
}
std::vector<Action> Monster::possible_dirs( const Map& map ) const
{
std::vector<Action> dirs;
if( x > 0 && !map.at( x / 64 - 1, y / 64 ).solid() )
dirs.push_back( Action::left );
if( x / 64 < map.getx() - 1 && !map.at( x / 64 + 1, y / 64 ).solid() )
dirs.push_back( Action::right );
if( y > 0 && !map.at( x / 64, y / 64 - 1 ).solid() )
dirs.push_back( Action::up );
if( y / 64 < map.gety() - 1 && !map.at( x / 64, y / 64 + 1 ).solid() )
dirs.push_back( Action::down );
return dirs;
}
bool Monster::straight( const std::vector<Action>& dirs )
{
return is_opposite( dirs[0], dirs[1] );
}
Action Monster::any_dir( const Map& map )
{
std::vector<Action> dirs = possible_dirs( map );
if( dirs.empty() )
return Action::wait;
return dirs[RNG::next( static_cast<int>( dirs.size() ) )];
}
Action Monster::rand_dir( const Map& map )
{
Action tmp = any_dir( map );
if( is_opposite( action, tmp ) )
tmp = any_dir( map );
return tmp;
}
void Monster::think( const Map& map )
{
ZoneScoped;
if( action == Action::wait || action == Action::appear )
{
set_action( rand_dir( map ) );
return;
}
std::vector<Action> dirs = possible_dirs( map );
if( dirs.size() == 2 && straight( dirs ) )
{
left = 64;
}
else
{
Action tmp = rand_dir( map );
if( tmp == action )
{
left = 64;
}
else
{
set_action( tmp );
if( tmp != Action::wait )
left = 64;
}
}
}
void Monster::tick( World& world )
{
ZoneScoped;
Map& map = world.map();
delta += Timer::delta;
while( delta > t )
{
delta -= t;
if( action == Action::wait )
{
think( map );
}
else if( left > 0 )
{
left--;
switch( action )
{
case Action::down: y++; break;
case Action::up: y--; break;
case Action::left: x--; break;
case Action::right: x++; break;
default: break;
}
}
else
{
if( action == Action::death )
die( world );
else
think( map );
}
if( action != Action::death && killed( map ) )
{
set_action( Action::death );
left = 790 / t;
}
}
}
void Monster::die( World& )
{
dead = true;
}
const AnimTexture& Monster::texture_for( Action a ) const
{
struct Set
{
const AnimTexture* wait;
const AnimTexture* up;
const AnimTexture* down;
const AnimTexture* left;
const AnimTexture* right;
const AnimTexture* death;
};
Set s;
if( mtype == 1 )
s = { &Textures::m1_d, &Textures::m1_u, &Textures::m1_d, &Textures::m1_l, &Textures::m1_r, &Textures::m1_death };
else if( mtype == 2 )
s = { &Textures::m2_d, &Textures::m2_u, &Textures::m2_d, &Textures::m2_l, &Textures::m2_r, &Textures::m2_death };
else
s = { &Textures::m3_d, &Textures::m3_u, &Textures::m3_d, &Textures::m3_l, &Textures::m3_r, &Textures::m3_death };
switch( a )
{
case Action::up: return *s.up;
case Action::down: return *s.down;
case Action::left: return *s.left;
case Action::right: return *s.right;
case Action::death: return *s.death;
case Action::wait:
case Action::appear:
default: return *s.wait; // wait/appear use the "down" sprite
}
}
void Monster::draw()
{
ZoneScoped;
// The original returns without drawing for unexpected actions; monsters only
// ever hold the actions handled by texture_for, so always draw.
generic_draw( texture_for( action ) );
}
void Monster::generic_draw( const AnimTexture& tex )
{
int frame;
if( action == Action::wait )
{
frame = 0;
}
else if( action == Action::appear )
{
frame = 0;
Gfx::alpha( static_cast<float>( 200 - left ) / 200.0f );
}
else
{
frame = static_cast<int>( ( Timer::get_timestamp() - action_start ) / 40 );
}
tex.bind( frame );
Gfx::draw_sprite( x, y );
if( action == Action::appear )
Gfx::alpha( 1.0f );
}
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include "entity.hpp"
#include <vector>
namespace dyna
{
class AnimTexture;
// The three monster variants from monster.cs differed only in speed, sprite set
// and respawn delay, so they fold into one class parameterised by `type` (1-3).
class Monster : public Entity
{
public:
Monster( int type, int gx, int gy );
void set_action( Action a ) override;
void tick( World& world ) override;
void draw() override;
void die( World& world ) override;
bool is_dead() const { return dead; }
int type() const { return mtype; }
private:
std::vector<Action> possible_dirs( const Map& map ) const;
static bool straight( const std::vector<Action>& dirs );
Action rand_dir( const Map& map );
Action any_dir( const Map& map ); // __rand_dir in the original
void think( const Map& map );
void generic_draw( const AnimTexture& tex );
const AnimTexture& texture_for( Action a ) const;
int mtype; // 1, 2 or 3
int t; // ms per movement sub-step (per-type speed)
bool dead = false;
};
}

View File

@@ -0,0 +1,127 @@
#include "player.hpp"
#include "gfx.hpp"
#include "map.hpp"
#include "texture.hpp"
#include "timer.hpp"
#include "world.hpp"
#include <tracy/Tracy.hpp>
namespace dyna
{
Player::Player( int gx, int gy )
{
x = gx * 64;
y = gy * 64;
set_action( Action::wait );
queue = Action::wait;
}
void Player::tick( World& world )
{
ZoneScoped;
Map& map = world.map();
delta += Timer::delta;
while( delta > t )
{
delta -= t;
if( left > 0 )
{
left--;
switch( action )
{
case Action::down: y++; break;
case Action::up: y--; break;
case Action::left: x--; break;
case Action::right: x++; break;
case Action::place_bomb:
if( left == 0 )
map.place_bomb( x / 64, y / 64 );
break;
default:
break;
}
}
else
{
if( action == Action::death )
{
die( world );
return;
}
if( map.at( x / 64, y / 64 ).kind == Field::Kind::vortex )
{
world.next_level = true;
return;
}
if( !can_move( queue, map ) )
queue = Action::wait;
if( action != queue )
set_action( queue );
if( action != Action::wait )
left = 64;
if( action == Action::place_bomb )
left = 32;
}
if( action != Action::death && killed( map ) )
{
set_action( Action::death );
left = 1140 / t;
}
}
}
void Player::draw()
{
ZoneScoped;
const AnimTexture* tex = nullptr;
switch( action )
{
case Action::wait: tex = &Textures::p_wait; break;
case Action::up: tex = &Textures::p_u; break;
case Action::down: tex = &Textures::p_d; break;
case Action::left: tex = &Textures::p_l; break;
case Action::right: tex = &Textures::p_r; break;
case Action::death: tex = &Textures::p_death; break;
case Action::place_bomb: tex = &Textures::p_wait; break;
default:
return;
}
int frame = static_cast<int>( Timer::get_timestamp() - action_start );
frame /= ( action == Action::death ) ? 60 : 40;
tex->bind( frame );
Gfx::draw_sprite( x, y );
}
void Player::move( Action a )
{
queue = a;
}
void Player::die( World& world )
{
world.killed = true;
}
bool Player::killed( const Map& map ) const
{
if( Entity::killed( map ) )
return true;
if( map.monster_collide( x, y ) )
return true;
return false;
}
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include "entity.hpp"
namespace dyna
{
class Player : public Entity
{
public:
Player( int gx, int gy );
void tick( World& world ) override;
void draw() override;
void die( World& world ) override;
void move( Action a ); // queues the next direction; applied between tiles
protected:
bool killed( const Map& map ) const override;
private:
static constexpr int t = 6; // ms per movement sub-step
Action queue = Action::wait;
};
}

View File

@@ -0,0 +1,221 @@
#include "texture.hpp"
#include "datapath.hpp"
#include "gfx.hpp"
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <tracy/Tracy.hpp>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <memory>
#include <vector>
namespace dyna
{
void GlTexture::reset()
{
if( id_ )
{
// The texture globals outlive main(), so their destructors can run after
// the GL context is already gone (which frees its textures anyway). Only
// call into GL while a context is current; otherwise just drop the name.
if( SDL_GL_GetCurrentContext() )
glDeleteTextures( 1, &id_ );
id_ = 0;
}
}
namespace
{
struct SurfaceDeleter
{
void operator()( SDL_Surface* s ) const { SDL_DestroySurface( s ); }
};
using SurfacePtr = std::unique_ptr<SDL_Surface, SurfaceDeleter>;
// Convert an arbitrary surface to tightly addressable RGBA8. Returns null on
// failure; the result owns its pixels.
SurfacePtr to_rgba( SDL_Surface* src )
{
ZoneScoped;
if( !src ) return nullptr;
return SurfacePtr{ SDL_ConvertSurface( src, SDL_PIXELFORMAT_RGBA32 ) };
}
} // namespace
bool Texture::load( const char* fn )
{
ZoneScoped;
ZoneText( fn, strlen( fn ) );
SurfacePtr image{ IMG_Load( fn ) };
if( !image )
{
std::fprintf( stderr, "Cannot open texture %s: %s\n", fn, SDL_GetError() );
return false;
}
SurfacePtr rgba = to_rgba( image.get() );
if( !rgba )
{
std::fprintf( stderr, "Cannot convert texture %s: %s\n", fn, SDL_GetError() );
return false;
}
// Pack the surface into a tight RGBA8 block, skipping any per-row padding.
const int w = rgba->w, h = rgba->h;
std::vector<std::uint8_t> packed( static_cast<size_t>( w ) * h * 4 );
const auto* pixels = static_cast<const std::uint8_t*>( rgba->pixels );
for( int row = 0; row < h; row++ )
{
std::memcpy( &packed[static_cast<size_t>( row ) * w * 4],
pixels + static_cast<size_t>( row ) * rgba->pitch,
static_cast<size_t>( w ) * 4 );
}
tex_ = GlTexture{ Render::make_texture( w, h, 1, packed.data() ) };
return static_cast<bool>( tex_ );
}
void Texture::bind() const
{
Render::use_texture( tex_.get(), 0 );
}
void AnimTexture::load( SDL_Surface* sheet, int tilex, int tiley, int n )
{
ZoneScoped;
SurfacePtr rgba = to_rgba( sheet );
if( !rgba )
{
std::fprintf( stderr, "Cannot convert sprite sheet: %s\n", SDL_GetError() );
return;
}
const auto* pixels = static_cast<const std::uint8_t*>( rgba->pixels );
const int pitch = rgba->pitch;
// Lay the n frames out back to back as the layers of an array texture.
constexpr int frame_bytes = 64 * 64 * 4;
std::vector<std::uint8_t> frames( static_cast<size_t>( n ) * frame_bytes );
for( int i = 0; i < n; i++ )
{
for( int fy = 0; fy < 64; fy++ )
{
int srcy = 64 * ( tiley + i ) + fy;
int srcx = 64 * tilex;
std::memcpy( &frames[static_cast<size_t>( i ) * frame_bytes + static_cast<size_t>( fy ) * 64 * 4],
pixels + static_cast<size_t>( srcy ) * pitch + static_cast<size_t>( srcx ) * 4,
static_cast<size_t>( 64 ) * 4 );
}
}
tex_ = GlTexture{ Render::make_texture( 64, 64, n, frames.data() ) };
frames_ = n;
}
void AnimTexture::bind( int frame ) const
{
if( frames_ <= 0 ) return;
int layer = frame % frames_;
if( layer < 0 ) layer += frames_;
Render::use_texture( tex_.get(), layer );
}
namespace Textures
{
Texture menu, sand, wall, crate;
AnimTexture p_wait, p_u, p_d, p_l, p_r, p_death;
AnimTexture bomb, bomb_appear, e_c, e_h, e_v, e_le, e_re, e_de, e_ue;
AnimTexture m1_death, m1_l, m1_r, m1_d, m1_u;
AnimTexture m2_death, m2_l, m2_r, m2_d, m2_u;
AnimTexture m3_death, m3_l, m3_r, m3_d, m3_u;
AnimTexture bonus1, bonus2;
AnimTexture vortex_appear, vortex;
void preload()
{
ZoneScoped;
menu.load( data_path( "data/gfx/menu.png" ).c_str() );
sand.load( data_path( "data/gfx/sand.png" ).c_str() );
wall.load( data_path( "data/gfx/wall.png" ).c_str() );
crate.load( data_path( "data/gfx/crate.png" ).c_str() );
{
SurfacePtr img{ IMG_Load( data_path( "data/gfx/Player.png" ).c_str() ) };
p_wait.load( img.get(), 0, 0, 20 );
p_d.load( img.get(), 1, 0, 20 );
p_u.load( img.get(), 2, 0, 20 );
p_l.load( img.get(), 3, 0, 20 );
p_r.load( img.get(), 4, 0, 20 );
p_death.load( img.get(), 5, 0, 20 );
}
{
SurfacePtr img{ IMG_Load( data_path( "data/gfx/Bomb.png" ).c_str() ) };
bomb.load( img.get(), 0, 0, 10 );
bomb_appear.load( img.get(), 5, 0, 10 );
e_c.load( img.get(), 1, 0, 5 );
e_h.load( img.get(), 2, 0, 5 );
e_v.load( img.get(), 1, 5, 5 );
e_le.load( img.get(), 3, 0, 5 );
e_re.load( img.get(), 2, 5, 5 );
e_de.load( img.get(), 4, 0, 5 );
e_ue.load( img.get(), 3, 5, 5 );
}
{
SurfacePtr img{ IMG_Load( data_path( "data/gfx/monster1.png" ).c_str() ) };
m1_death.load( img.get(), 0, 0, 20 );
m1_u.load( img.get(), 1, 0, 10 );
m1_l.load( img.get(), 2, 0, 10 );
m1_d.load( img.get(), 1, 10, 10 );
m1_r.load( img.get(), 2, 10, 10 );
}
{
SurfacePtr img{ IMG_Load( data_path( "data/gfx/monster2.png" ).c_str() ) };
m2_death.load( img.get(), 0, 0, 20 );
m2_d.load( img.get(), 1, 0, 20 );
m2_u.load( img.get(), 2, 0, 20 );
m2_l.load( img.get(), 3, 0, 20 );
m2_r.load( img.get(), 4, 0, 20 );
}
{
SurfacePtr img{ IMG_Load( data_path( "data/gfx/monster3.png" ).c_str() ) };
m3_death.load( img.get(), 0, 0, 20 );
m3_d.load( img.get(), 1, 0, 9 );
m3_u.load( img.get(), 2, 0, 9 );
m3_l.load( img.get(), 1, 10, 9 );
m3_r.load( img.get(), 2, 10, 9 );
}
{
SurfacePtr img{ IMG_Load( data_path( "data/gfx/bonusy.png" ).c_str() ) };
bonus1.load( img.get(), 0, 0, 20 );
bonus2.load( img.get(), 1, 0, 20 );
}
{
SurfacePtr img{ IMG_Load( data_path( "data/gfx/portal.png" ).c_str() ) };
vortex_appear.load( img.get(), 0, 0, 20 );
vortex.load( img.get(), 1, 0, 20 );
}
}
}
}

View File

@@ -0,0 +1,91 @@
#pragma once
#include <glad/gl.h>
struct SDL_Surface;
namespace dyna
{
// Move-only RAII owner of a GL texture name. Every texture in the game is a
// GL_TEXTURE_2D_ARRAY (static images use a single layer, animations use one
// layer per frame) so the renderer only ever has to deal with one sampler type.
class GlTexture
{
public:
GlTexture() = default;
explicit GlTexture( GLuint id ) noexcept : id_( id ) {}
~GlTexture() { reset(); }
GlTexture( GlTexture&& o ) noexcept : id_( o.id_ ) { o.id_ = 0; }
GlTexture& operator=( GlTexture&& o ) noexcept
{
if( this != &o )
{
reset();
id_ = o.id_;
o.id_ = 0;
}
return *this;
}
GlTexture( const GlTexture& ) = delete;
GlTexture& operator=( const GlTexture& ) = delete;
GLuint get() const { return id_; }
explicit operator bool() const { return id_ != 0; }
void reset(); // glDeleteTextures; safe on an empty handle
private:
GLuint id_ = 0;
};
// A single static texture loaded from a whole image file. Ported from
// texture.cs; binding just records the texture for the next draw call.
class Texture
{
public:
bool load( const char* fn );
void bind() const;
private:
GlTexture tex_;
};
// A vertical strip of 64x64 animation frames cut out of a sprite sheet, stored
// as the layers of one array texture. Mirrors AnimTexture in texture.cs.
class AnimTexture
{
public:
// Extract n frames from column `tilex`, starting at row `tiley`, where each
// coordinate is in 64px tile units. Mirrors AnimTexture.load in texture.cs.
void load( SDL_Surface* sheet, int tilex, int tiley, int n );
void bind( int frame ) const; // frame is taken modulo the frame count
private:
GlTexture tex_;
int frames_ = 0;
};
// All game textures, loaded once at startup. Mirrors the Textures class.
namespace Textures
{
extern Texture menu, sand, wall, crate;
extern AnimTexture p_wait, p_u, p_d, p_l, p_r, p_death;
extern AnimTexture bomb, bomb_appear, e_c, e_h, e_v, e_le, e_re, e_de, e_ue;
extern AnimTexture m1_death, m1_l, m1_r, m1_d, m1_u;
extern AnimTexture m2_death, m2_l, m2_r, m2_d, m2_u;
extern AnimTexture m3_death, m3_l, m3_r, m3_d, m3_u;
extern AnimTexture bonus1, bonus2;
extern AnimTexture vortex_appear, vortex;
void preload();
}
}

View File

@@ -0,0 +1,51 @@
#include "timer.hpp"
#include <SDL3/SDL.h>
#include <random>
namespace dyna
{
namespace Timer
{
int delta = 0;
static std::int64_t timestamp = 0;
void reset()
{
delta = 0;
timestamp = static_cast<std::int64_t>( SDL_GetTicks() );
}
int tick()
{
std::int64_t tmp = timestamp;
timestamp = static_cast<std::int64_t>( SDL_GetTicks() );
delta = static_cast<int>( timestamp - tmp );
return delta;
}
std::int64_t get_timestamp()
{
return timestamp;
}
}
namespace RNG
{
static std::mt19937& engine()
{
static std::mt19937 e{ std::random_device{}() };
return e;
}
int next( int n )
{
if( n <= 0 ) return 0;
std::uniform_int_distribution<int> dist( 0, n - 1 );
return dist( engine() );
}
}
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <cstdint>
namespace dyna
{
// Frame timing, ported from timer.cs. Timestamps are milliseconds since
// Timer::reset(); kept 64-bit so the modulo arithmetic the animation code
// relies on never overflows during a session.
namespace Timer
{
void reset();
int tick(); // advances the clock, returns delta in ms
std::int64_t get_timestamp();
extern int delta; // ms elapsed during the last tick()
}
// Thin wrapper over a single global PRNG, mirroring the C# RNG helper.
namespace RNG
{
int next( int n ); // uniform in [0, n)
}
}

View File

@@ -0,0 +1,40 @@
#include "world.hpp"
#include "map.hpp"
#include "player.hpp"
namespace dyna
{
World::World( const std::string& level_fn, bool with_player )
: map_( std::make_unique<Map>( level_fn ) )
, name_( level_fn.substr( level_fn.rfind( '/' ) + 1 ) )
{
if( with_player )
{
player_ = map_->create_player();
crates_left = map_->get_crates();
}
else
{
crates_left = -1; // the menu never opens an exit portal
}
}
World::~World() = default;
void World::tick()
{
map_->tick( *this );
if( player_ )
player_->tick( *this );
}
void World::draw()
{
map_->draw();
if( player_ )
player_->draw();
}
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <memory>
#include <string>
namespace dyna
{
class Map;
class Player;
// Owns the state for one running level: the map, the player (absent on the
// menu screen), and the flags the gameplay code used to reach through global
// variables. Passing a World& into the tick path replaces the old Game::p /
// Game::current_map / Game::killed globals, so there are no non-owning pointers
// to outlive the objects they point at.
class World
{
public:
// Loads `level_fn`; spawns a player from the map's '@' marker when
// with_player is set (gameplay) and leaves it null otherwise (menu).
World( const std::string& level_fn, bool with_player );
~World();
World( const World& ) = delete;
World& operator=( const World& ) = delete;
Map& map() { return *map_; }
const Map& map() const { return *map_; }
Player* player() { return player_.get(); } // null on the menu screen
const std::string& name() const { return name_; }
void tick();
void draw();
bool killed = false;
bool next_level = false;
int crates_left = 0;
private:
std::unique_ptr<Map> map_;
std::unique_ptr<Player> player_;
std::string name_;
};
}

View File

@@ -0,0 +1,83 @@
# CMakeLists.txt — OpenGL spinning triangle demo
#
# macOS:
# cmake -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -B build/ninja .
# cmake --build build/ninja
#
# Linux (requires libsdl3-dev libgl1-mesa-dev):
# cmake -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -B build/ninja .
# cmake --build build/ninja
#
# Windows:
# cmake -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -B build/ninja .
# cmake --build build/ninja
cmake_minimum_required(VERSION 3.16)
project(gl_spinning_triangle LANGUAGES C CXX)
# ---------------------------------------------------------------------------
# Tracy root — defaults to three directories above this CMakeLists.txt.
# ---------------------------------------------------------------------------
set(TRACY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../..")
option(TRACY_ENABLE "Enable Tracy profiling" ON)
# ---------------------------------------------------------------------------
# Platform — SDL3 (cross-platform windowing, must be installed on the system)
# ---------------------------------------------------------------------------
find_package(SDL3 REQUIRED)
# ---------------------------------------------------------------------------
# GL extension loader — GLEW (Windows + Linux, fetched automatically)
# ---------------------------------------------------------------------------
if(NOT APPLE)
include(FetchContent)
set(glew-cmake_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(ONLY_LIBS ON CACHE BOOL "" FORCE)
FetchContent_Declare(glew
GIT_REPOSITORY https://github.com/Perlmint/glew-cmake.git
GIT_TAG master # pin to a specific commit for reproducible builds
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(glew)
endif()
set(PLATFORM_SOURCES platform/platform_sdl3.cpp)
if(APPLE)
set(PLATFORM_LIBS SDL3::SDL3 "-framework OpenGL")
elseif(WIN32)
set(PLATFORM_LIBS SDL3::SDL3 opengl32 libglew_static)
else()
set(PLATFORM_LIBS SDL3::SDL3 GL libglew_static)
endif()
# ---------------------------------------------------------------------------
# Target
# ---------------------------------------------------------------------------
add_executable(gl_spinning_triangle
spinning_triangle.cpp
"${TRACY_DIR}/public/TracyClient.cpp"
${PLATFORM_SOURCES}
)
# Suppress upstream warnings from TracyClient.cpp
if(MSVC)
set_source_files_properties("${TRACY_DIR}/public/TracyClient.cpp"
PROPERTIES COMPILE_FLAGS "/w"
)
else()
set_source_files_properties("${TRACY_DIR}/public/TracyClient.cpp"
PROPERTIES COMPILE_FLAGS "-w"
)
endif()
target_compile_features(gl_spinning_triangle PRIVATE cxx_std_17)
if(TRACY_ENABLE)
target_compile_definitions(gl_spinning_triangle PRIVATE TRACY_ENABLE)
endif()
target_include_directories(gl_spinning_triangle PRIVATE
"${TRACY_DIR}/public"
)
target_link_libraries(gl_spinning_triangle PRIVATE ${PLATFORM_LIBS})

View File

@@ -0,0 +1,37 @@
// platform.h — interface between platform-agnostic code and platform backends
//
// Each platform_*.mm / platform_*.cpp file implements these four functions.
// Exactly one backend must be linked into the final binary.
#pragma once
#ifdef __APPLE__
# include <OpenGL/gl3.h>
#else
# include <GL/glew.h>
#endif
// Initialize the windowing system, create a window, and make an OpenGL 3.3
// Core Profile context current on the calling thread.
// Returns true on success.
bool platformInit(int width, int height, const char* title);
// Load OpenGL function pointers (no-op on macOS where the framework exports them directly).
// Must be called after platformInit() while the GL context is current.
// Returns true on success.
bool platformInitGL();
// Elapsed wall-clock time in seconds since platformInit().
double platformGetTime();
// Swap front and back buffers (present the rendered frame).
void platformSwapBuffers();
// Pixel scaling factor relative to the logical window size (1.0 on non-HiDPI displays).
// Must be called after platformInit().
void platformGetPixelDensityScale(float* x, float* y);
// Enter the platform event/render loop.
// Calls render() each frame at ~60 fps.
// Calls shutdown() exactly once before returning.
void platformRunLoop(void (*render)(), void (*shutdown)());

View File

@@ -0,0 +1,85 @@
// platform_sdl3.cpp — SDL3 windowing backend (cross-platform)
#include "platform.h" // GL headers first (gl3.h / glew.h) so SDL sees guards set
#define SDL_MAIN_HANDLED // we don't want SDL_main
#include <SDL3/SDL.h>
#include <chrono>
#include <cstdio>
static SDL_Window* sWin = nullptr;
static SDL_GLContext sCtx = nullptr;
static std::chrono::steady_clock::time_point sStartTime;
bool platformInit(int width, int height, const char* title) {
if (!SDL_Init(SDL_INIT_VIDEO)) {
fprintf(stderr, "ERROR: SDL_Init failed: %s\n", SDL_GetError());
return false;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
sWin = SDL_CreateWindow(title, width, height, SDL_WINDOW_OPENGL);
if (!sWin) {
fprintf(stderr, "ERROR: SDL_CreateWindow failed: %s\n", SDL_GetError());
SDL_Quit();
return false;
}
SDL_SetWindowPosition(sWin, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
sCtx = SDL_GL_CreateContext(sWin);
if (!sCtx) {
fprintf(stderr, "ERROR: SDL_GL_CreateContext failed: %s\n", SDL_GetError());
SDL_DestroyWindow(sWin);
SDL_Quit();
return false;
}
SDL_GL_SetSwapInterval(1);
sStartTime = std::chrono::steady_clock::now();
return true;
}
bool platformInitGL() {
#ifndef __APPLE__
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK) {
fprintf(stderr, "Failed to initialize GLEW\n");
return false;
}
#endif
return true;
}
double platformGetTime() {
return std::chrono::duration<double>(
std::chrono::steady_clock::now() - sStartTime).count();
}
void platformSwapBuffers() { SDL_GL_SwapWindow(sWin); }
void platformGetPixelDensityScale(float* x, float* y) {
int pw, ph, ww, wh;
SDL_GetWindowSizeInPixels(sWin, &pw, &ph);
SDL_GetWindowSize(sWin, &ww, &wh);
*x = (ww > 0) ? (float)pw / (float)ww : 1.0f;
*y = (wh > 0) ? (float)ph / (float)wh : 1.0f;
}
void platformRunLoop(void (*render)(), void (*shutdown)()) {
bool running = true;
while (running) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_QUIT) running = false;
if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) running = false;
}
if (running) render();
}
shutdown();
SDL_GL_DestroyContext(sCtx);
SDL_DestroyWindow(sWin);
SDL_Quit();
}

View File

@@ -0,0 +1,145 @@
// spinning_triangle.cpp — OpenGL spinning triangle demo with Tracy GPU profiling.
#ifdef __APPLE__
// NOTE: OpenGL is only available on MacOS (no iOS support)
// Including and using anything related to OpenGL on Apple (like <OpenGL/gl3.h>)
// will emit deprecation warnings, unless GL_SILENCE_DEPRECATION is defined
#define GL_SILENCE_DEPRECATION
// NOTE: TracyOpenGL.hpp will not work as expected even on Apple devices that
// support OpenGL, because the OpenGL drivers do not implement ARB_timer_query
// properly (querying GL_TIMESTAMP always resolves to 0). TracyOpenGL.hpp will
// emit a compiler warning, and a Tracy message to the trace/profiler, but the
// program will still run.
#endif
#include "platform/platform.h" // also includes OpenGL headers
#include <tracy/Tracy.hpp>
// NOTE: opt-in toggle for periodic recalibrations during Collect()
#define TRACY_OPENGL_AUTO_CALIBRATION
#include <tracy/TracyOpenGL.hpp>
static const int kWidth = 800;
static const int kHeight = 600;
static GLuint gProgram = 0;
static GLuint gVao = 0;
static GLint gAngleLoc = -1;
// Vertex colors and positions are baked in; rotation is driven by a uniform.
static const char* kVertSrc = R"(
#version 150 core
uniform float uAngle;
const vec2 kPos[3] = vec2[3](
vec2( 0.0, 0.5 ),
vec2(-0.433, -0.25 ),
vec2( 0.433, -0.25 )
);
const vec3 kCol[3] = vec3[3](
vec3(1.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0),
vec3(0.0, 0.0, 1.0)
);
out vec3 vColor;
void main() {
float c = cos(uAngle);
float s = sin(uAngle);
vec2 p = kPos[gl_VertexID];
gl_Position = vec4(p.x*c - p.y*s, p.x*s + p.y*c, 0.0, 1.0);
vColor = kCol[gl_VertexID];
}
)";
static const char* kFragSrc = R"(
#version 150 core
in vec3 vColor;
out vec4 fragColor;
void main() { fragColor = vec4(vColor, 1.0); }
)";
static GLuint compileShader(GLenum type, const char* src) {
GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, nullptr);
glCompileShader(s);
GLint ok = 0;
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[512];
glGetShaderInfoLog(s, sizeof(log), nullptr, log);
fprintf(stderr, "Shader compile error: %s\n", log);
glDeleteShader(s);
return 0;
}
return s;
}
static int initGL() {
if (!platformInitGL()) return 1;
TracyGpuContext;
TracyGpuContextName("OpenGL", 6);
GLuint vert = compileShader(GL_VERTEX_SHADER, kVertSrc);
GLuint frag = compileShader(GL_FRAGMENT_SHADER, kFragSrc);
if (!vert || !frag) return 1;
gProgram = glCreateProgram();
glAttachShader(gProgram, vert);
glAttachShader(gProgram, frag);
glLinkProgram(gProgram);
glDeleteShader(vert);
glDeleteShader(frag);
GLint ok = 0;
glGetProgramiv(gProgram, GL_LINK_STATUS, &ok);
if (!ok) {
char log[512];
glGetProgramInfoLog(gProgram, sizeof(log), nullptr, log);
fprintf(stderr, "Program link error: %s\n", log);
return 1;
}
gAngleLoc = glGetUniformLocation(gProgram, "uAngle");
// Core profile requires a bound VAO even with no vertex attributes.
glGenVertexArrays(1, &gVao);
glBindVertexArray(gVao);
glClearColor(0.05f, 0.05f, 0.08f, 1.0f);
float scaleX, scaleY;
platformGetPixelDensityScale(&scaleX, &scaleY);
glViewport(0, 0, (int)(kWidth * scaleX), (int)(kHeight * scaleY));
return 0;
}
static void renderFrame() {
ZoneScoped;
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(gProgram);
{
TracyGpuZone("triangle draw");
glUniform1f(gAngleLoc, (float)platformGetTime());
glDrawArrays(GL_TRIANGLES, 0, 3);
}
platformSwapBuffers();
TracyGpuCollect;
}
static void shutdown() {
fprintf(stderr, "application is shutting down...\n");
glDeleteVertexArrays(1, &gVao);
glDeleteProgram(gProgram);
}
int main() {
if (!platformInit(kWidth, kHeight, "OpenGL Spinning Triangle"))
return 1;
if (initGL() != 0)
return 2;
platformRunLoop(renderFrame, shutdown);
return 0;
}

View File

@@ -0,0 +1,157 @@
# CMakeLists.txt — WebGPU spinning triangle demo
#
# macOS:
# clang++ -std=c++17 -ObjC++ spinning_triangle.cpp platform/platform_macos.mm \
# -I/path/to/wgpu/include -L/path/to/wgpu/lib -lwgpu_native \
# -Wl,-rpath,@executable_path \
# -framework Cocoa -framework Metal -framework QuartzCore \
# -framework Foundation -framework IOKit -framework IOSurface \
# -o spinning_triangle
#
# Windows (MSVC):
# cl /std:c++17 spinning_triangle.cpp platform/platform_windows.cpp \
# /I\path\to\wgpu\include \path\to\wgpu\lib\wgpu_native.lib \
# user32.lib gdi32.lib /Fe:spinning_triangle.exe
#
# Linux (requires libsdl3-dev):
# g++ -std=c++17 spinning_triangle.cpp platform/platform_wayland.cpp \
# xdg-shell-protocol.c \
# -I/path/to/wgpu/include -L/path/to/wgpu/lib -lwgpu_native \
# -lwayland-client -o spinning_triangle
cmake_minimum_required(VERSION 3.16)
project(spinning_triangle LANGUAGES C CXX)
# ---------------------------------------------------------------------------
# WebGPU backend — set WGPU_PATH to your wgpu-native or Dawn installation.
# The library name differs between backends:
# wgpu-native → wgpu_native
# Dawn → webgpu_dawn
# ---------------------------------------------------------------------------
set(WGPU_PATH "" CACHE PATH "Root of the WebGPU native installation (contains include/ and lib/)")
set(WGPU_LIB "" CACHE STRING "WebGPU library name (wgpu_native or webgpu_dawn); auto-detected if empty")
if(NOT WGPU_PATH)
message(FATAL_ERROR "Set WGPU_PATH to the root of your WebGPU native installation.")
endif()
# When WGPU_PATH changes, discard any previously auto-detected WGPU_LIB so
# detection re-runs against the new path.
if(NOT "${WGPU_PATH}" STREQUAL "${_WGPU_PATH_LAST}")
unset(WGPU_LIB CACHE)
set(WGPU_LIB "" CACHE STRING "WebGPU library name (wgpu_native or webgpu_dawn); auto-detected if empty")
endif()
set(_WGPU_PATH_LAST "${WGPU_PATH}" CACHE INTERNAL "")
if(NOT WGPU_LIB)
unset(_WGPU_NATIVE_LIB CACHE)
unset(_WEBGPU_DAWN_LIB CACHE)
find_library(_WGPU_NATIVE_LIB NAMES wgpu_native wgpu_native.dll PATHS "${WGPU_PATH}/lib" NO_DEFAULT_PATH)
find_library(_WEBGPU_DAWN_LIB NAMES webgpu_dawn PATHS "${WGPU_PATH}/lib" NO_DEFAULT_PATH)
if(_WGPU_NATIVE_LIB)
set(WGPU_LIB "wgpu_native" CACHE STRING "WebGPU library name (wgpu_native or webgpu_dawn); auto-detected if empty" FORCE)
elseif(_WEBGPU_DAWN_LIB)
set(WGPU_LIB "webgpu_dawn" CACHE STRING "WebGPU library name (wgpu_native or webgpu_dawn); auto-detected if empty" FORCE)
else()
message(FATAL_ERROR "Could not detect a WebGPU library in ${WGPU_PATH}/lib. Set WGPU_LIB explicitly (wgpu_native or webgpu_dawn).")
endif()
message(STATUS "WebGPU library auto-detected: ${WGPU_LIB}")
endif()
# ---------------------------------------------------------------------------
# Tracy root — defaults to two directories above this CMakeLists.txt.
# ---------------------------------------------------------------------------
set(TRACY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../..")
option(TRACY_ENABLE "Enable Tracy profiling" ON)
# ---------------------------------------------------------------------------
# macOS quarantine — pre-built WebGPU binaries downloaded from the internet
# carry a com.apple.quarantine extended attribute that prevents dyld from
# loading them ("damaged or incomplete" / Gatekeeper block). Strip it once
# at configure time so the linker and the runtime loader can both access the
# library directory without further user intervention.
# ---------------------------------------------------------------------------
if(APPLE)
execute_process(
COMMAND xattr -dr com.apple.quarantine "${WGPU_PATH}/lib"
)
endif()
# ---------------------------------------------------------------------------
# Platform — SDL3 (cross-platform windowing, must be installed on the system)
# ---------------------------------------------------------------------------
find_package(SDL3 REQUIRED)
set(PLATFORM_SOURCES platform/platform_sdl3.cpp)
if(APPLE)
set(PLATFORM_LIBS
SDL3::SDL3
"-framework Cocoa"
"-framework Metal"
"-framework QuartzCore"
"-framework Foundation"
"-framework IOKit"
"-framework IOSurface"
)
elseif(WIN32)
# wgpu-native (Rust stdlib) pull-ins: NtReadFile, GetUserProfileDirectoryW, ...
set(WGPU_NATIVE_WIN32_LIBS ntdll userenv)
# Dawn pull-ins: WKPDID_D3DDebugObjectName GUID, CompareObjectHandles, ...
set(WEBGPU_DAWN_WIN32_LIBS dxguid onecore)
set(PLATFORM_LIBS SDL3::SDL3 ${WGPU_NATIVE_WIN32_LIBS} ${WEBGPU_DAWN_WIN32_LIBS})
else()
set(PLATFORM_LIBS SDL3::SDL3)
endif()
# ---------------------------------------------------------------------------
# Target
# ---------------------------------------------------------------------------
add_executable(spinning_triangle
spinning_triangle.cpp
"${TRACY_DIR}/public/TracyClient.cpp"
${PLATFORM_SOURCES}
)
# Treat TracyClient.cpp as third-party code — suppress all warnings so that
# upstream changes don't pollute our build output.
if(MSVC)
set_source_files_properties("${TRACY_DIR}/public/TracyClient.cpp"
PROPERTIES COMPILE_FLAGS "/w"
)
else()
set_source_files_properties("${TRACY_DIR}/public/TracyClient.cpp"
PROPERTIES COMPILE_FLAGS "-w"
)
endif()
target_compile_features(spinning_triangle PRIVATE cxx_std_17)
if(TRACY_ENABLE)
target_compile_definitions(spinning_triangle PRIVATE TRACY_ENABLE)
endif()
target_include_directories(spinning_triangle PRIVATE
"${WGPU_PATH}/include"
"${TRACY_DIR}/public"
)
target_link_directories(spinning_triangle PRIVATE "${WGPU_PATH}/lib")
target_link_libraries(spinning_triangle PRIVATE
${WGPU_LIB}
${PLATFORM_LIBS}
)
# Embed the rpath so the binary finds the WebGPU dylib/so next to itself.
if(APPLE)
set_target_properties(spinning_triangle PROPERTIES
BUILD_RPATH "${WGPU_PATH}/lib"
INSTALL_RPATH "@executable_path"
)
elseif(UNIX)
set_target_properties(spinning_triangle PROPERTIES
BUILD_RPATH "${WGPU_PATH}/lib"
INSTALL_RPATH "$ORIGIN"
)
endif()

View File

@@ -0,0 +1,23 @@
// platform.h — interface between platform-agnostic code and platform backends
//
// Each platform_*.mm / platform_*.cpp file implements these five functions.
// Exactly one backend must be linked into the final binary.
#pragma once
#include <webgpu/webgpu.h>
// Initialize the windowing system and create a window of the given dimensions.
// Returns true on success.
bool platformInit(int width, int height, const char* title);
// Create a WebGPU surface backed by the platform window.
// Must be called after wgpuCreateInstance() and platformInit().
WGPUSurface platformCreateSurface(WGPUInstance instance);
// Elapsed wall-clock time in seconds since platformInit().
double platformGetTime();
// Enter the platform event/render loop.
// Calls render() each frame at ~60 fps.
// Calls shutdown() exactly once before returning.
void platformRunLoop(void (*render)(), void (*shutdown)());

View File

@@ -0,0 +1,95 @@
// platform_sdl3.cpp — SDL3 windowing backend for the WebGPU example
#include "platform.h" // webgpu/webgpu.h first
#define SDL_MAIN_HANDLED // we don't want SDL_main
#include <SDL3/SDL.h>
#ifdef __APPLE__
# include <SDL3/SDL_metal.h>
#endif
#include <chrono>
#include <cstdio>
static SDL_Window* sWin = nullptr;
static std::chrono::steady_clock::time_point sStartTime;
#ifdef __APPLE__
static SDL_MetalView sMetalView = nullptr;
#endif
bool platformInit(int width, int height, const char* title) {
if (!SDL_Init(SDL_INIT_VIDEO)) {
fprintf(stderr, "ERROR: SDL_Init failed: %s\n", SDL_GetError());
return false;
}
SDL_WindowFlags flags = 0;
#ifdef __APPLE__
flags |= SDL_WINDOW_METAL;
#endif
sWin = SDL_CreateWindow(title, width, height, flags);
if (!sWin) {
fprintf(stderr, "ERROR: SDL_CreateWindow failed: %s\n", SDL_GetError());
SDL_Quit();
return false;
}
SDL_SetWindowPosition(sWin, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
sStartTime = std::chrono::steady_clock::now();
return true;
}
WGPUSurface platformCreateSurface(WGPUInstance instance) {
WGPUSurfaceDescriptor desc = {};
SDL_PropertiesID props = SDL_GetWindowProperties(sWin);
#if defined(__APPLE__)
sMetalView = SDL_Metal_CreateView(sWin);
if (!sMetalView) {
fprintf(stderr, "ERROR: SDL_Metal_CreateView failed\n");
return nullptr;
}
WGPUSurfaceSourceMetalLayer metalDesc = {};
metalDesc.chain.sType = WGPUSType_SurfaceSourceMetalLayer;
metalDesc.layer = SDL_Metal_GetLayer(sMetalView);
desc.nextInChain = &metalDesc.chain;
#elif defined(_WIN32)
WGPUSurfaceSourceWindowsHWND hwndDesc = {};
hwndDesc.chain.sType = WGPUSType_SurfaceSourceWindowsHWND;
hwndDesc.hinstance = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_INSTANCE_POINTER, nullptr);
hwndDesc.hwnd = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
desc.nextInChain = &hwndDesc.chain;
#else // Linux / X11
WGPUSurfaceSourceXlibWindow x11Desc = {};
x11Desc.chain.sType = WGPUSType_SurfaceSourceXlibWindow;
x11Desc.display = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, nullptr);
x11Desc.window = (uint32_t)SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
desc.nextInChain = &x11Desc.chain;
#endif
return wgpuInstanceCreateSurface(instance, &desc);
}
double platformGetTime() {
return std::chrono::duration<double>(
std::chrono::steady_clock::now() - sStartTime).count();
}
void platformRunLoop(void (*render)(), void (*shutdown)()) {
bool running = true;
while (running) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_QUIT) running = false;
if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) running = false;
}
if (running) render();
}
shutdown();
#ifdef __APPLE__
SDL_Metal_DestroyView(sMetalView);
#endif
SDL_DestroyWindow(sWin);
SDL_Quit();
}

View File

@@ -0,0 +1,352 @@
// spinning_triangle.cpp — platform-agnostic WebGPU spinning triangle demo.
#include "platform/platform.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <webgpu/webgpu.h>
#include <tracy/Tracy.hpp>
#include <tracy/TracyWebGPU.hpp>
// ---------------------------------------------------------------------------
// Globals
// ---------------------------------------------------------------------------
static const int kWidth = 800;
static const int kHeight = 600;
static WGPUInstance gInstance = nullptr;
static WGPUSurface gSurface = nullptr;
static WGPUAdapter gAdapter = nullptr;
static WGPUDevice gDevice = nullptr;
static WGPUQueue gQueue = nullptr;
static WGPURenderPipeline gPipeline = nullptr;
static WGPUBuffer gUniformBuf = nullptr;
static WGPUBindGroup gBindGroup = nullptr;
static TracyWebGPUCtx gTracyCtx = nullptr;
static WGPUTextureFormat gSurfaceFormat = WGPUTextureFormat_BGRA8Unorm;
// TODO: this can become platformError() instead
int error(int code, const char* message) {
fprintf(stderr, "ERROR: %s (code: %d)\n", message, code);
return code;
}
// ---------------------------------------------------------------------------
// WGSL shader — vertex colours baked in, rotation via a uniform float.
// ---------------------------------------------------------------------------
static const char* kShaderSource = R"(
struct Uniforms {
angle: f32,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
struct VSOut {
@builtin(position) pos: vec4f,
@location(0) color: vec3f,
};
@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VSOut {
var positions = array<vec2f, 3>(
vec2f( 0.0, 0.5),
vec2f(-0.433, -0.25),
vec2f( 0.433, -0.25),
);
var colors = array<vec3f, 3>(
vec3f(1.0, 0.0, 0.0),
vec3f(0.0, 1.0, 0.0),
vec3f(0.0, 0.0, 1.0),
);
let c = cos(u.angle);
let s = sin(u.angle);
let p = positions[vi];
let rotated = vec2f(p.x * c - p.y * s, p.x * s + p.y * c);
var out: VSOut;
out.pos = vec4f(rotated, 0.0, 1.0);
out.color = colors[vi];
return out;
}
@fragment
fn fs_main(@location(0) color: vec3f) -> @location(0) vec4f {
return vec4f(color, 1.0);
}
)";
// ---------------------------------------------------------------------------
// Adapter / Device request callbacks (current wgpu-native API)
// ---------------------------------------------------------------------------
static void onAdapterReady(WGPURequestAdapterStatus status,
WGPUAdapter adapter,
WGPUStringView message,
void* userdata1, void* /*userdata2*/) {
if (status == WGPURequestAdapterStatus_Success) {
*(WGPUAdapter*)userdata1 = adapter;
} else {
fprintf(stderr, "Adapter request failed: %.*s\n",
(int)message.length, message.data);
}
}
static void onDeviceReady(WGPURequestDeviceStatus status,
WGPUDevice device,
WGPUStringView message,
void* userdata1, void* /*userdata2*/) {
if (status == WGPURequestDeviceStatus_Success) {
*(WGPUDevice*)userdata1 = device;
} else {
fprintf(stderr, "Device request failed: %.*s\n",
(int)message.length, message.data);
}
}
// ---------------------------------------------------------------------------
// WebGPU init
// ---------------------------------------------------------------------------
static int initWebGPU() {
// Adapter
WGPURequestAdapterOptions adapterOpts = {};
adapterOpts.compatibleSurface = gSurface;
WGPURequestAdapterCallbackInfo adapterCB = {};
adapterCB.mode = WGPUCallbackMode_AllowProcessEvents;
adapterCB.callback = onAdapterReady;
adapterCB.userdata1 = &gAdapter;
wgpuInstanceRequestAdapter(gInstance, &adapterOpts, adapterCB);
while (!gAdapter) { wgpuInstanceProcessEvents(gInstance); }
if (!gAdapter) return error(11, "No adapter");
WGPUUncapturedErrorCallbackInfo errorCB = {};
errorCB.callback = [](WGPUDevice const*, WGPUErrorType type,
WGPUStringView message, void*, void*) {
fprintf(stderr, "[WGPU ERROR] type=%d %.*s\n",
(int)type, (int)message.length, message.data);
};
WGPUDeviceDescriptor deviceDesc = {};
deviceDesc.uncapturedErrorCallbackInfo = errorCB;
TracyWebGPUSetupDeviceDescriptor(deviceDesc);
WGPURequestDeviceCallbackInfo deviceCB = {};
deviceCB.mode = WGPUCallbackMode_AllowProcessEvents;
deviceCB.callback = onDeviceReady;
deviceCB.userdata1 = &gDevice;
wgpuAdapterRequestDevice(gAdapter, &deviceDesc, deviceCB);
while (!gDevice) { wgpuInstanceProcessEvents(gInstance); }
if (!gDevice) return error(12, "No device");
gQueue = wgpuDeviceGetQueue(gDevice);
gTracyCtx = TracyWebGPUContext(gInstance, gDevice, gQueue);
TracyWebGPUContextName(gTracyCtx, "WebGPU", 6);
// Configure surface
WGPUSurfaceConfiguration config = {};
config.device = gDevice;
config.format = gSurfaceFormat;
config.usage = WGPUTextureUsage_RenderAttachment;
config.alphaMode = WGPUCompositeAlphaMode_Opaque;
config.width = kWidth;
config.height = kHeight;
config.presentMode = WGPUPresentMode_Fifo;
wgpuSurfaceConfigure(gSurface, &config);
// Shader module
WGPUShaderSourceWGSL wgslSrc = {};
wgslSrc.chain.sType = WGPUSType_ShaderSourceWGSL;
wgslSrc.code = { kShaderSource, WGPU_STRLEN };
WGPUShaderModuleDescriptor smDesc = {};
smDesc.nextInChain = (WGPUChainedStruct*)&wgslSrc;
WGPUShaderModule shaderMod = wgpuDeviceCreateShaderModule(gDevice, &smDesc);
// Uniform buffer (one f32 for rotation angle)
WGPUBufferDescriptor bufDesc = {};
bufDesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
bufDesc.size = sizeof(float);
gUniformBuf = wgpuDeviceCreateBuffer(gDevice, &bufDesc);
// Bind group layout + bind group
WGPUBindGroupLayoutEntry bglEntry = {};
bglEntry.binding = 0;
bglEntry.visibility = WGPUShaderStage_Vertex;
bglEntry.buffer.type = WGPUBufferBindingType_Uniform;
bglEntry.buffer.minBindingSize = sizeof(float);
WGPUBindGroupLayoutDescriptor bglDesc = {};
bglDesc.entryCount = 1;
bglDesc.entries = &bglEntry;
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(gDevice, &bglDesc);
WGPUBindGroupEntry bgEntry = {};
bgEntry.binding = 0;
bgEntry.buffer = gUniformBuf;
bgEntry.size = sizeof(float);
WGPUBindGroupDescriptor bgDesc = {};
bgDesc.layout = bgl;
bgDesc.entryCount = 1;
bgDesc.entries = &bgEntry;
gBindGroup = wgpuDeviceCreateBindGroup(gDevice, &bgDesc);
// Pipeline layout
WGPUPipelineLayoutDescriptor plDesc = {};
plDesc.bindGroupLayoutCount = 1;
plDesc.bindGroupLayouts = &bgl;
WGPUPipelineLayout pipelineLayout = wgpuDeviceCreatePipelineLayout(gDevice, &plDesc);
// Render pipeline
WGPUColorTargetState colorTarget = {};
colorTarget.format = gSurfaceFormat;
colorTarget.writeMask = WGPUColorWriteMask_All;
WGPUFragmentState fragState = {};
fragState.module = shaderMod;
fragState.entryPoint = { "fs_main", WGPU_STRLEN };
fragState.targetCount = 1;
fragState.targets = &colorTarget;
WGPURenderPipelineDescriptor rpDesc = {};
rpDesc.layout = pipelineLayout;
rpDesc.vertex.module = shaderMod;
rpDesc.vertex.entryPoint = { "vs_main", WGPU_STRLEN };
rpDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
rpDesc.multisample.count = 1;
rpDesc.multisample.mask = 0xFFFFFFFF;
rpDesc.fragment = &fragState;
gPipeline = wgpuDeviceCreateRenderPipeline(gDevice, &rpDesc);
// Cleanup intermediates
wgpuShaderModuleRelease(shaderMod);
wgpuPipelineLayoutRelease(pipelineLayout);
wgpuBindGroupLayoutRelease(bgl);
return 0;
}
// ---------------------------------------------------------------------------
// Frame rendering
// ---------------------------------------------------------------------------
// Returns the surface texture for the current frame, or {.texture=nullptr} on
// a skippable condition (timeout, occlusion) or an error.
static WGPUSurfaceTexture getWindowSurface() {
WGPUSurfaceTexture surfTex = {};
wgpuSurfaceGetCurrentTexture(gSurface, &surfTex);
if (surfTex.status == WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal ||
surfTex.status == WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal)
return surfTex;
// Timeout and Occluded are normal OS events (window covered / on a different Space).
bool silent = surfTex.status == WGPUSurfaceGetCurrentTextureStatus_Timeout;
#ifdef WGPU_H_
silent = silent || surfTex.status == (WGPUSurfaceGetCurrentTextureStatus)WGPUSurfaceGetCurrentTextureStatus_Occluded;
#endif
if (!silent)
fprintf(stderr, "Failed to get surface texture (status %d)\n", surfTex.status);
if (surfTex.texture) wgpuTextureRelease(surfTex.texture);
surfTex.texture = nullptr;
return surfTex;
}
static void renderFrame() {
ZoneScoped;
// Update rotation angle
float angle = (float)platformGetTime();
wgpuQueueWriteBuffer(gQueue, gUniformBuf, 0, &angle, sizeof(float));
WGPUSurfaceTexture surfTex = getWindowSurface();
if (!surfTex.texture) return;
WGPUTextureView view = wgpuTextureCreateView(surfTex.texture, nullptr);
// Command encoder
WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(gDevice, nullptr);
// Render pass
WGPURenderPassColorAttachment colorAtt = {};
colorAtt.view = view;
colorAtt.loadOp = WGPULoadOp_Clear;
colorAtt.storeOp = WGPUStoreOp_Store;
colorAtt.clearValue = { 0.05, 0.05, 0.08, 1.0 };
colorAtt.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
WGPURenderPassDescriptor passDesc = {};
passDesc.colorAttachmentCount = 1;
passDesc.colorAttachments = &colorAtt;
{
ZoneScopedN("render-pass");
TracyWebGPUNamedZone(gTracyCtx, tracyZone, encoder, passDesc, "triangle draw", true);
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(encoder, &passDesc);
wgpuRenderPassEncoderSetPipeline(pass, gPipeline);
wgpuRenderPassEncoderSetBindGroup(pass, 0, gBindGroup, 0, nullptr);
wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
wgpuRenderPassEncoderEnd(pass);
wgpuRenderPassEncoderRelease(pass);
}
// Submit
WGPUCommandBuffer cmdBuf = wgpuCommandEncoderFinish(encoder, nullptr);
wgpuQueueSubmit(gQueue, 1, &cmdBuf);
// Present
wgpuSurfacePresent(gSurface);
// Process Events
wgpuInstanceProcessEvents(gInstance);
TracyWebGPUCollect(gTracyCtx);
// Cleanup
wgpuCommandBufferRelease(cmdBuf);
wgpuCommandEncoderRelease(encoder);
wgpuTextureViewRelease(view);
wgpuTextureRelease(surfTex.texture);
}
// ---------------------------------------------------------------------------
// Shutdown
// ---------------------------------------------------------------------------
static void shutdown() {
fprintf(stderr, "application is shutting down...\n");
TracyWebGPUDestroy(gTracyCtx);
if (gBindGroup) wgpuBindGroupRelease(gBindGroup);
if (gUniformBuf) wgpuBufferRelease(gUniformBuf);
if (gPipeline) wgpuRenderPipelineRelease(gPipeline);
if (gQueue) wgpuQueueRelease(gQueue);
if (gDevice) wgpuDeviceRelease(gDevice);
if (gAdapter) wgpuAdapterRelease(gAdapter);
if (gSurface) wgpuSurfaceRelease(gSurface);
if (gInstance) wgpuInstanceRelease(gInstance);
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int main(int argc, char* argv[]) {
if (!platformInit(kWidth, kHeight, "WebGPU Spinning Triangle"))
return 1;
gInstance = wgpuCreateInstance(nullptr);
if (!gInstance) return error(2, "Failed to create WebGPU instance.");
gSurface = platformCreateSurface(gInstance);
if (!gSurface) return error(3, "Failed to create surface.");
if (initWebGPU() != 0) return 4;
platformRunLoop(renderFrame, shutdown);
return 0;
}

View File

@@ -2,7 +2,20 @@
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/tracy">
<comment>Tracy Profiler trace file</comment>
<comment xml:lang="ar">ملف تتبع لمحلل الأداء Tracy</comment>
<comment xml:lang="de">Trace-Datei des Tracy-Profilers</comment>
<comment xml:lang="es">Archivo de traza del perfilador Tracy</comment>
<comment xml:lang="fr">Fichier de trace du profileur Tracy</comment>
<comment xml:lang="hi">Tracy प्रोफ़ाइलर ट्रेस फ़ाइल</comment>
<comment xml:lang="it">File di traccia del profiler Tracy</comment>
<comment xml:lang="ja">Tracy プロファイラーのトレースファイル</comment>
<comment xml:lang="pl">Zrzut sesji profilera Tracy</comment>
<comment xml:lang="pt">Ficheiro de rastreio do perfilador Tracy</comment>
<comment xml:lang="pt_BR">Arquivo de rastreamento do perfilador Tracy</comment>
<comment xml:lang="ru">Файл трассировки профилировщика Tracy</comment>
<comment xml:lang="uk">Файл трасування профілювальника Tracy</comment>
<comment xml:lang="zh_CN">Tracy 性能分析器跟踪文件</comment>
<comment xml:lang="zh_TW">Tracy 效能分析器追蹤檔案</comment>
<icon name="application-tracy"/>
<magic>
<match type="string" value="tlZ\x04" offset="0"/>

View File

@@ -1,12 +1,53 @@
[Desktop Entry]
Version=1.0
Version=1.5
Type=Application
Name=Tracy Profiler
GenericName=Code profiler
GenericName[ar]=محلل أداء الشيفرة البرمجية
GenericName[de]=Code-Profiler
GenericName[es]=Perfilador de código
GenericName[fr]=Profileur de code
GenericName[hi]=कोड प्रोफ़ाइलर
GenericName[it]=Profiler del codice
GenericName[ja]=コードプロファイラー
GenericName[pl]=Profiler kodu
GenericName[pt]=Analisador de desempenho de código
GenericName[pt_BR]=Analisador de desempenho de código
GenericName[ru]=Профилировщик кода
GenericName[uk]=Профілювальник коду
GenericName[zh_CN]=代码性能分析器
GenericName[zh_TW]=程式碼效能分析器
Comment=Examine code to see where it is slow
Comment[pl]=Znajdowanie wolno wykonującego się kodu
Exec=/usr/bin/tracy %f
Comment[ar]=افحص الشيفرة البرمجية لمعرفة مواضع البطء
Comment[de]=Code untersuchen, um langsame Stellen zu finden
Comment[es]=Examine el código para ver dónde es lento
Comment[fr]=Examiner le code pour voir où il est lent
Comment[hi]=कोड की जाँच करें कि वह कहाँ धीमा है
Comment[it]=Esamina il codice per vedere dove è lento
Comment[ja]=コードを調べて遅い箇所を見つける
Comment[pl]=Przeanalizuj kod, aby znaleźć wolne miejsca
Comment[pt]=Examine o código para ver onde é lento
Comment[pt_BR]=Examine o código para ver onde ele é lento
Comment[ru]=Изучите код, чтобы найти медленные места
Comment[uk]=Досліджуйте код, щоб знайти повільні місця
Comment[zh_CN]=检查代码,找出运行缓慢的部分
Comment[zh_TW]=檢查程式碼,找出執行緩慢的部分
Keywords=profiler;performance;tracing;frame;instrumentation;
Keywords[ar]=محلل;أداء;تتبع;إطار;قياس;
Keywords[de]=Profiler;Leistung;Tracing;Frame;Instrumentierung;
Keywords[es]=perfilador;rendimiento;trazado;fotograma;instrumentación;
Keywords[fr]=profileur;performance;traçage;trame;instrumentation;
Keywords[hi]=प्रोफ़ाइलर;प्रदर्शन;ट्रेसिंग;फ़्रेम;इंस्ट्रुमेंटेशन;
Keywords[it]=profiler;prestazioni;tracciamento;frame;strumentazione;
Keywords[ja]=プロファイラー;パフォーマンス;トレース;フレーム;計測;
Keywords[pl]=profiler;wydajność;śledzenie;klatka;instrumentacja;
Keywords[pt]=perfilador;desempenho;rastreio;fotograma;instrumentação;
Keywords[pt_BR]=perfilador;desempenho;rastreamento;quadro;instrumentação;
Keywords[ru]=профилировщик;производительность;трассировка;кадр;инструментирование;
Keywords[uk]=профілювальник;продуктивність;трасування;кадр;інструментування;
Keywords[zh_CN]=分析器;性能;跟踪;帧;插桩;
Keywords[zh_TW]=分析器;效能;追蹤;影格;插樁;
Exec=tracy-profiler %f
Icon=tracy
Terminal=false
Categories=Development;Profiling;

View File

@@ -1,7 +1,7 @@
# Tracy MCP eval guide
This document covers the bindings-layer detail that the curated catalog
(`tracy://catalog`) and analysis guidance (`tracy://prompt`) do not.
This document covers the bindings-layer detail that the analysis
guidance (`tracy://prompt`) does not.
## ctx
@@ -21,8 +21,15 @@ data surface. Common entry points:
- Threads: `get_threads()`, `get_thread_name(tid)`, `get_thread_context_switches(tid)`
- Messages / plots / locks / memory / callstacks: `get_messages()`, `get_plots()`,
`get_locks()`, `get_memory_events()`, `get_callstack_frames(...)`
- Sections: `get_sections()` — timed code sections from
`TracySectionEnter`/`TracySectionLeave` instrumentation. Returns a list of
`{category, start, end, text}` dicts (start/end in ns); `category` is the
section's named group (multiple unrelated section sets can coexist in one
capture).
- Capture metadata: `get_capture_name()`, `get_capture_program()`,
`get_first_time()`, `get_last_time()`, `get_resolution()`, `get_host_info()`
- Readiness: `is_background_done()` — see "Instance lifecycle" below before
querying zone/symbol stats right after `load_capture`.
Run `print([m for m in dir(ctx) if not m.startswith('_')])` for the full list.
@@ -41,24 +48,23 @@ Run `print([m for m in dir(ctx) if not m.startswith('_')])` for the full list.
- Source-location IDs from `get_all_zone_source_locations()` are the join key
between zone-name lookups and per-callsite queries.
## Translating catalog entries to ctx Python
## Common query patterns
The catalog (`tracy://catalog`) lists curated queries. Each maps to a small
Python snippet:
Small Python snippets for the queries you'll reach for most often:
```python
# zone_list — top 10 hottest zones by total time
# top 10 hottest zones by total time
top = sorted(ctx.get_all_zone_stats().items(),
key=lambda kv: kv[1].total, reverse=True)[:10]
for k, v in top:
print(f"{v.total/1e6:.2f}ms count={v.count} {k}")
# frame_list — primary frame set timing
# primary frame set timing
times = ctx.get_frame_times() # ns per frame
print(f"frames={len(times)} avg={sum(times)/len(times)/1e6:.2f}ms "
f"p99={sorted(times)[int(len(times)*0.99)]/1e6:.2f}ms")
# zone_stats for a named zone — find the srcloc id, then drill in
# stats for a named zone — find the srcloc id, then drill in
import re
matches = [k for k in ctx.get_all_zone_stats() if k.startswith("MyFunc ")]
sid = int(re.search(r"<(\d+)>$", matches[0]).group(1))
@@ -70,3 +76,40 @@ stats = ctx.get_zone_stats(sid)
For long-running queries pass `async_mode=True` to `eval`; it returns
`{task_id, status: "running"}`. Poll with the `task` tool
(`action="poll", task_id=...`).
## Instance lifecycle
`tracy_mcp.py` runs as a long-lived singleton process shared across every
MCP client (all VS Code windows), and every `live_connect`/`load_capture`
instance holds its *entire* trace — zones, messages, callstacks, memory
events — resident in memory for as long as that process runs. Nothing
frees it just because your conversation ends.
- `load_capture` returns before zone/symbol statistics finish building — a
background thread populates them after the file itself is read. Check
`is_background_done()` (or `background_done` in `list_instances`) before
relying on `get_all_zone_stats()` and similar stats-based queries; they
can return empty or partial results until it's true. For small captures
this finishes before you'd notice; for multi-GB ones it can take seconds.
- Call `unload_capture(instance_id)` as soon as you're done analyzing a
capture. Don't rely on automatic eviction as your primary cleanup path —
treat it as a backstop for forgotten sessions, not a substitute.
- `live_connect` caps a live session at `TRACY_MCP_LIVE_MEMORY_LIMIT_MB`
(default 8192, override per-call or set 0 to disable) — a long-lived
session on a busy target otherwise grows unbounded and can OOM-kill the
whole server process. Hitting the cap disconnects that instance cleanly;
data collected so far stays queryable and `save_trace`-able.
- `list_instances` reports `idle_seconds`, `connected`, and `background_done`
per instance; use it to spot stale ones before starting a new session,
especially if you're about to load several captures for comparison.
- The server provides three backstops, all configurable via environment
variables at startup: it evicts the least-recently-used *evictable*
instance (disconnected live sessions, or any loaded file capture — never
a still-connected live one) once the instance count reaches
`TRACY_MCP_MAX_INSTANCES` (default 4); it drops a disconnected live
instance that's sat idle for `TRACY_MCP_DISCONNECTED_TTL_S` (default
1800s / 30 min) so a session is still there for analysis right after the
target disconnects, but doesn't linger forever if forgotten; and it drops
a file-loaded capture that's sat idle for `TRACY_MCP_FILE_IDLE_TTL_S`
(default 1800s) — safe to let expire since it's already durably on disk,
just `load_capture` it again.

View File

@@ -0,0 +1 @@
mcp>=2.0.0,<3

View File

@@ -1,6 +1,8 @@
#!/bin/sh
# Start the Tracy MCP server.
#
# First run: pip install -r requirements.txt (next to this script).
#
# Set PYTHONPATH to the directory containing TracyServerBindings.so/.pyd.
# Adjust the Release/Debug suffix to match your CMake build configuration.
PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$(dirname "$0")/../../build/python/Release"

View File

@@ -5,6 +5,7 @@ import asyncio
import atexit
import builtins
import concurrent.futures
import faulthandler
import glob
import io
import os
@@ -14,10 +15,12 @@ import socket
import struct
import sys
import time
import urllib.error
import urllib.request
import uuid
from contextlib import redirect_stdout
from contextlib import asynccontextmanager, redirect_stdout
import mcp.server.fastmcp as fastmcp
from mcp.server.mcpserver import MCPServer
# Suppress noisy ASGI shutdown errors known to occur with SSE and Control-C.
# These occur when Starlette attempts to send a 500 error after the loop is cancelled
@@ -29,7 +32,13 @@ logging.getLogger("starlette").setLevel(logging.CRITICAL)
_HERE = os.path.dirname(os.path.abspath(__file__))
_PORT_FILE = os.path.join(_HERE, "tracy_mcp.port")
_PID_FILE = os.path.join(_HERE, "tracy_mcp.pid")
_CRASH_LOG_FILE = os.path.join(_HERE, "tracy_mcp.crash.log")
_PREFERRED_PORT = int(os.environ.get("TRACY_MCP_PORT", "47380"))
_TRANSPORT = os.environ.get("TRACY_MCP_TRANSPORT", "streamable-http").strip().lower()
# MCPServer.run()'s own defaults -- tracked here too since v2's Settings no
# longer exposes them for us to read back (SDK v1 -> v2 migration).
_SSE_PATH = "/sse"
_STREAMABLE_HTTP_PATH = "/mcp"
# Shared documentation surfaces. system.prompt.md is Tracy Assist's source
# system prompt; exposing it as an MCP resource keeps analysis guidance in
@@ -155,10 +164,26 @@ async def _listen_broadcasts(timeout_s: float = 1.5) -> list[dict]:
return list(seen.values())
def _is_our_server_running() -> tuple[bool, int]:
def _http_ping(port: int, timeout_s: float = 2.0) -> bool:
"""Confirms the server answers HTTP, not just that its PID exists — a
deadlocked process still passes os.kill(pid, 0). Any response, even an
error one, proves the transport is alive; only a timeout or refused
connection means it's not.
"""
Check the PID file to see if our server is already running.
Returns (running, port). Uses os.kill(pid, 0) to confirm the process is alive.
path = _SSE_PATH if _TRANSPORT == "sse" else _STREAMABLE_HTTP_PATH
try:
urllib.request.urlopen(f"http://127.0.0.1:{port}{path}", timeout=timeout_s)
return True
except urllib.error.HTTPError:
return True
except Exception:
return False
def _is_our_server_running() -> tuple[bool, int]:
"""Check whether our server is running: PID alive AND responding to
an HTTP self-ping (a hung process still passes os.kill(pid, 0)).
Returns (running, port).
"""
try:
with open(_PID_FILE) as f:
@@ -166,9 +191,19 @@ def _is_our_server_running() -> tuple[bool, int]:
with open(_PORT_FILE) as f:
port = int(f.read().strip())
os.kill(pid, 0) # raises OSError if process is gone
return True, port
except Exception:
return False, 0
if not _http_ping(port):
print(
f"Tracy MCP process {pid} is alive (PID check passed) but isn't "
f"responding on port {port} within {2.0:.0f}s -- likely hung/deadlocked, "
f"not just busy. Starting a fresh server on a new port; kill PID {pid} "
f"manually once you're able to (it's still holding the port and the "
f"TracyServerBindings.pyd file lock).",
file=sys.stderr,
)
return False, 0
return True, port
def _find_free_port() -> int:
@@ -218,8 +253,11 @@ except ImportError:
except ImportError:
tracy_server = None
mcp_server = fastmcp.FastMCP("Tracy Profiler")
executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
_MAX_INSTANCES = int(os.environ.get("TRACY_MCP_MAX_INSTANCES", "4"))
_DISCONNECTED_TTL_S = float(os.environ.get("TRACY_MCP_DISCONNECTED_TTL_S", "1800"))
_FILE_IDLE_TTL_S = float(os.environ.get("TRACY_MCP_FILE_IDLE_TTL_S", "1800"))
_SWEEP_INTERVAL_S = float(os.environ.get("TRACY_MCP_SWEEP_INTERVAL_S", "300"))
_DEFAULT_LIVE_MEMORY_LIMIT_MB = int(os.environ.get("TRACY_MCP_LIVE_MEMORY_LIMIT_MB", "8192"))
class Task:
@@ -239,7 +277,126 @@ class TracyInstance:
self.worker = worker
self.path = None
self.mtime = None
self.last_used = time.time()
# Wall-clock time the instance was first observed disconnected (live
# instances only). None while connected or for file-loaded captures.
self.disconnected_since: float | None = None
def touch(self) -> None:
self.last_used = time.time()
def is_evictable(self) -> bool:
"""True if this instance is safe to drop without losing in-progress data.
A live instance still connected is actively recording — never evict it
automatically. Everything else (file captures, disconnected live
instances) is fair game for the LRU cap.
"""
if self.worker is not None and self.path is None:
try:
return not self.worker.is_connected()
except Exception:
return True
return True
def _shutdown_worker(worker: object | None) -> None:
if worker is None:
return
try:
worker.shutdown()
except Exception:
pass
def _evict_idle() -> list[str]:
"""Unload instances idle past their TTL: disconnected live instances
past _DISCONNECTED_TTL_S, file-loaded captures past _FILE_IDLE_TTL_S
(safe — already durably on disk). Connected live instances are never
touched here.
"""
now = time.time()
evicted = []
for name, inst in list(instances.items()):
if inst.path is not None:
if now - inst.last_used >= _FILE_IDLE_TTL_S:
del instances[name]
_shutdown_worker(inst.worker)
evicted.append(name)
continue
if inst.worker is None:
continue
try:
connected = inst.worker.is_connected()
except Exception:
connected = False
if connected:
inst.disconnected_since = None
continue
if inst.disconnected_since is None:
inst.disconnected_since = now
continue
if now - inst.disconnected_since >= _DISCONNECTED_TTL_S:
del instances[name]
_shutdown_worker(inst.worker)
evicted.append(name)
return evicted
def _evict_for_capacity(exclude: str | None = None) -> str | None:
"""If at/over _MAX_INSTANCES, drop the least-recently-used evictable
instance to make room for a new one. Returns the evicted name, if any."""
if len(instances) < _MAX_INSTANCES:
return None
candidates = [
inst for name, inst in instances.items()
if name != exclude and inst.is_evictable()
]
if not candidates:
return None
victim = min(candidates, key=lambda inst: inst.last_used)
del instances[victim.name]
_shutdown_worker(victim.worker)
return victim.name
async def _sweep_loop() -> None:
"""Periodic eviction sweep, doubling as a heartbeat — distinguishes a
crashed process (see faulthandler) from a hung one (stops producing
these lines). flush=True since a hang is exactly when buffering would
hide the last known-good timestamp.
"""
start = time.time()
while True:
await asyncio.sleep(_SWEEP_INTERVAL_S)
try:
evicted = _evict_idle()
except Exception:
evicted = None
print(
f"[heartbeat] uptime={int(time.time() - start)}s "
f"instances={len(instances)} tasks={len(tasks)} "
f"evicted={evicted or 'none'}",
file=sys.stderr,
flush=True,
)
@asynccontextmanager
async def _lifespan(_server: MCPServer):
sweep_task = asyncio.create_task(_sweep_loop())
try:
yield
finally:
sweep_task.cancel()
try:
await sweep_task
except asyncio.CancelledError:
pass
mcp_server = MCPServer("Tracy Profiler", lifespan=_lifespan)
executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
instances: dict[str, TracyInstance] = {}
tasks: dict[str, Task] = {}
@@ -258,8 +415,7 @@ def _prompt_resource() -> str:
@mcp_server.resource("tracy://eval-guide")
def _eval_guide_resource() -> str:
"""Bindings-layer guide for the eval tool: ctx object model, time units,
source-location ID semantics, and worked examples translating catalog
entries into ctx Python."""
source-location ID semantics, and worked examples of common ctx queries."""
return _read_text(_EVAL_GUIDE_PATH)
@@ -273,13 +429,34 @@ async def list_captures() -> list[str]:
@mcp_server.tool()
async def list_instances() -> list[dict]:
"""List all loaded Tracy instances and captures with metadata."""
"""List all loaded Tracy instances and captures with metadata.
Each full trace stays resident in memory for as long as this server
process runs (it's a singleton shared across all MCP clients) until
unloaded. `idle_seconds` is how long since this instance was last used
via `eval`/`save_trace` — call `unload_capture` on ones you no longer
need instead of waiting for automatic eviction. Automatic eviction kicks
in at the `TRACY_MCP_MAX_INSTANCES` cap (LRU, connected live instances
are never evicted), after a disconnected live instance sits idle past
`TRACY_MCP_DISCONNECTED_TTL_S`, or after a file-loaded capture sits idle
past `TRACY_MCP_FILE_IDLE_TTL_S` — it's already durably on disk, so
nothing is lost; just `load_capture` it again.
`background_done: false` means a just-loaded capture is still building
zone/symbol statistics on a background thread — `get_all_zone_stats`
and similar stats-based `eval` queries can return empty or partial
results until this flips to true.
"""
now = time.time()
return [
{
"id": name,
"path": inst.path,
"mtime": inst.mtime,
"live": inst.path is None
"live": inst.path is None,
"connected": inst.worker.is_connected() if inst.path is None and inst.worker else None,
"background_done": inst.worker.is_background_done() if inst.worker else None,
"idle_seconds": now - inst.last_used,
}
for name, inst in instances.items()
]
@@ -311,11 +488,25 @@ async def discover_instances(port_range: str = "8086-8095") -> list[dict]:
@mcp_server.tool()
async def live_connect(address: str = "127.0.0.1", port: int = 8086, alias: str | None = None) -> str:
async def live_connect(
address: str = "127.0.0.1",
port: int = 8086,
alias: str | None = None,
memory_limit_mb: int | None = None,
) -> str:
"""
Connect to a live running Tracy-instrumented application.
Wraps Worker(addr, port, memoryLimit=-1). Returns the instance_id.
Wraps Worker(addr, port, memoryLimit). A long-lived session on a busy
target grows unbounded otherwise -- every zone/message/memory event
stays resident until disconnect, which can OOM-kill this whole process
rather than just the one instance. memory_limit_mb defaults to
TRACY_MCP_LIVE_MEMORY_LIMIT_MB (8192 if unset); pass 0 to disable.
Hitting the limit disconnects that Worker cleanly (Tracy's own
QueryTerminate path) -- already-collected data stays queryable and
save_trace-able, it just stops growing.
Returns the instance_id.
"""
if not tracy_server:
return "Error: Tracy Server bindings not found."
@@ -337,8 +528,10 @@ async def live_connect(address: str = "127.0.0.1", port: int = 8086, alias: str
f"the target against a matching Tracy version."
)
limit_mb = _DEFAULT_LIVE_MEMORY_LIMIT_MB if memory_limit_mb is None else memory_limit_mb
memory_limit = limit_mb * 1024 * 1024 if limit_mb > 0 else -1
try:
w = tracy_server.Worker(address, port)
w = tracy_server.Worker(address, port, memory_limit)
except Exception as e:
return f"Failed to connect: {str(e)}"
@@ -382,9 +575,14 @@ async def live_connect(address: str = "127.0.0.1", port: int = 8086, alias: str
)
name = alias or f"live_{address}_{port}"
if name in instances:
_shutdown_worker(instances[name].worker)
evicted = _evict_for_capacity(exclude=name)
instances[name] = TracyInstance(name, w)
note = f" (evicted idle instance '{evicted}' to stay under the {_MAX_INSTANCES}-instance cap)" if evicted else ""
limit_note = f"{limit_mb}MB memory limit" if limit_mb > 0 else "no memory limit"
return (
f"Connected to live instance as '{name}'. "
f"Connected to live instance as '{name}'{note} ({limit_note}). "
f"Before your first eval, read resources tracy://prompt "
f"(analysis guidance) and tracy://eval-guide (ctx object model, "
f"ns time units, srcloc IDs)."
@@ -404,6 +602,12 @@ async def load_capture(path: str, alias: str | None = None) -> str:
If you don't already have a path, call `list_captures` first — it lists
.tracy files in the TRACY_CAPTURES_DIR environment directory.
Loading returns before zone/symbol statistics finish building — a
background thread populates them after the file itself is read. Check
`background_done` in `list_instances` (or poll it) before relying on
`get_all_zone_stats` and similar stats-based `eval` queries; they can
return empty or partial results until it's true.
"""
if not tracy_server:
return "Error: Tracy Server bindings not found."
@@ -418,7 +622,11 @@ async def load_capture(path: str, alias: str | None = None) -> str:
if name in instances:
inst = instances[name]
if inst.path == path and inst.mtime == mtime:
inst.touch()
return f"Instance '{name}' is already loaded and up to date."
_shutdown_worker(inst.worker)
evicted = _evict_for_capacity(exclude=name)
f = tracy_server.open_file(path)
w = tracy_server.create_worker_from_file(f)
@@ -426,8 +634,9 @@ async def load_capture(path: str, alias: str | None = None) -> str:
inst.path = path
inst.mtime = mtime
instances[name] = inst
note = f" (evicted idle instance '{evicted}' to stay under the {_MAX_INSTANCES}-instance cap)" if evicted else ""
return (
f"Loaded as '{name}'. "
f"Loaded as '{name}'{note}. "
f"Before your first eval, read resources tracy://prompt "
f"(analysis guidance) and tracy://eval-guide (ctx object model, "
f"ns time units, srcloc IDs)."
@@ -476,6 +685,7 @@ async def save_trace(
instance = instances[instance_id]
if not instance.worker:
return f"Error: Instance '{instance_id}' has no worker."
instance.touch()
if not os.path.isabs(path):
if captures_dir and os.path.basename(path) == path:
@@ -542,9 +752,18 @@ async def _execute_save(worker: object, path: str, level: int, streams: int, fi_
@mcp_server.tool()
async def unload_capture(instance_id: str) -> str:
"""Unload a Tracy instance and release its memory."""
"""Unload a Tracy instance and release its memory.
Prefer calling this as soon as you're done analyzing a capture — every
loaded instance holds the *entire* trace (zones, messages, callstacks,
memory events) in memory for as long as this server process runs, and
the server is a long-lived singleton shared across all MCP clients.
Automatic eviction exists as a backstop (see `list_instances`), not a
substitute for unloading what you no longer need.
"""
if instance_id in instances:
del instances[instance_id]
inst = instances.pop(instance_id)
_shutdown_worker(inst.worker)
return f"Instance '{instance_id}' unloaded."
return f"Instance '{instance_id}' not found."
@@ -566,6 +785,7 @@ async def tracy_eval(code: str, instance_id: str, async_mode: bool = False) -> o
instance = instances[instance_id]
if not instance.worker:
return f"Error: Instance '{instance_id}' has no worker."
instance.touch()
if not async_mode:
return await _execute_eval(code, instance.worker)
@@ -675,8 +895,22 @@ async def shutdown_server() -> str:
if __name__ == "__main__":
# Enabled before anything touches the native bindings, so a genuine
# fatal crash (segfault etc.) writes a thread-state traceback to disk
# before the process dies — works on Windows via
# SetUnhandledExceptionFilter, same mechanism as POSIX signals.
_crash_log_fh = open(_CRASH_LOG_FILE, "a", encoding="utf-8")
faulthandler.enable(file=_crash_log_fh, all_threads=True)
atexit.register(_cleanup_pid_files)
if _TRANSPORT not in ("sse", "streamable-http"):
print(
"TRACY_MCP_TRANSPORT must be 'sse' or 'streamable-http'.",
file=sys.stderr,
)
sys.exit(1)
running, existing_port = _is_our_server_running()
if running:
print(
@@ -689,12 +923,18 @@ if __name__ == "__main__":
port = _find_free_port()
_write_pid_and_port(port)
print(f"Tracy MCP listening on http://127.0.0.1:{port}/sse", file=sys.stderr)
path = _SSE_PATH if _TRANSPORT == "sse" else _STREAMABLE_HTTP_PATH
print(f"Tracy MCP listening on http://127.0.0.1:{port}{path}", file=sys.stderr)
mcp_server.settings.host = "127.0.0.1"
mcp_server.settings.port = port
# v2's MCPServer takes transport options on run(), not the constructor
# or a mutable settings object (SDK v1 -> v2 migration).
run_kwargs = {"host": "127.0.0.1", "port": port}
if _TRANSPORT == "sse":
run_kwargs["sse_path"] = _SSE_PATH
else:
run_kwargs["streamable_http_path"] = _STREAMABLE_HTTP_PATH
try:
mcp_server.run(transport="sse")
mcp_server.run(transport=_TRANSPORT, **run_kwargs)
except KeyboardInterrupt:
print("\nTracy MCP server stopped.", file=sys.stderr)
sys.exit(0)

21
extra/tracy-version.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
# Derive the project version from the single source of truth:
# public/common/TracyVersion.hpp. Prints X.Y.Z, exits nonzero on failure.
header="$1"
[ -r "$header" ] || {
echo "tracy-version: cannot read $header" >&2
exit 1
}
major=$(sed -n 's/.*Major = \([0-9][0-9]*\).*/\1/p' "$header")
minor=$(sed -n 's/.*Minor = \([0-9][0-9]*\).*/\1/p' "$header")
patch=$(sed -n 's/.*Patch = \([0-9][0-9]*\).*/\1/p' "$header")
if [ -n "$major" ] && [ -n "$minor" ] && [ -n "$patch" ]; then
printf '%s.%s.%s\n' "$major" "$minor" "$patch"
else
echo "tracy-version: could not parse version from $header" >&2
exit 1
fi

View File

@@ -1,13 +0,0 @@
#!/bin/sh
version_header="../public/common/TracyVersion.hpp"
major=$(grep -o -E 'Major = [0-9]+' "$version_header" | awk -F '= ' '{print $2}')
minor=$(grep -o -E 'Minor = [0-9]+' "$version_header" | awk -F '= ' '{print $2}')
patch=$(grep -o -E 'Patch = [0-9]+' "$version_header" | awk -F '= ' '{print $2}')
version="${major}.${minor}.${patch}"
# the extension is required for macOS's outdated sed
sed -i.bak "s/version: '[0-9]*\.[0-9]*\.[0-9]*'/version: '$version'/g" ../meson.build
rm ../meson.build.bak

31
import/CMakePresets.json Normal file
View File

@@ -0,0 +1,31 @@
{
"version": 10,
"configurePresets": [
{
"name": "common",
"hidden": true,
"binaryDir": "${sourceDir}/build-${presetName}",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
},
{
"name": "debug",
"displayName": "Debug configuration",
"inherits": "common",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "release",
"displayName": "Release configuration",
"inherits": "common",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}

View File

@@ -3,3 +3,151 @@ function Link(el)
el.attributes['reference'] = nil
return el
end
-- Drop Div wrappers (e.g. table/titlepage containers), keeping their content.
function Div(el)
return el.content
end
-- ---------------------------------------------------------------------------
-- LaTeX math -> plain-text approximation.
--
-- The target Markdown renderer has no math support, so a raw "$\frac{1}{2}$"
-- would show verbatim. We turn each math node into the closest Unicode/ASCII
-- equivalent: fractions become "a/b", \times becomes "x", super/subscripts use
-- Unicode digits, and the one multi-line display equation becomes a fenced
-- code block (Markdown collapses plain newlines, a code block keeps them).
-- ---------------------------------------------------------------------------
local sup = {['0']='',['1']='¹',['2']='²',['3']='³',['4']='',['5']='',
['6']='',['7']='',['8']='',['9']='',['+']='',['-']='',
['=']='',['(']='',[')']=''}
local sub = {['0']='',['1']='',['2']='',['3']='',['4']='',['5']='',
['6']='',['7']='',['8']='',['9']='',['+']='',['-']='',
['=']='',['(']='',[')']=''}
-- Symbol replacements, applied as literal substitutions. Longer commands must
-- precede those that are a prefix of them (e.g. \rightarrow before \right).
local symbols = {
{'\\leftrightarrow',''}, {'\\rightarrow',''}, {'\\leftarrow',''},
{'\\Rightarrow',''}, {'\\Leftarrow',''}, {'\\to',''}, {'\\mapsto',''},
{'\\times','×'}, {'\\cdot','·'}, {'\\div','÷'}, {'\\ast','*'}, {'\\star','*'},
{'\\leq',''}, {'\\geq',''}, {'\\neq',''}, {'\\approx',''}, {'\\equiv',''},
{'\\ll','«'}, {'\\gg','»'}, {'\\le',''}, {'\\ge',''},
{'\\ldots',''}, {'\\cdots',''}, {'\\dots',''}, {'\\infty',''},
{'\\pm','±'}, {'\\mp',''}, {'\\propto',''}, {'\\sum','Σ'}, {'\\prod','Π'},
{'\\alpha','α'}, {'\\beta','β'}, {'\\gamma','γ'}, {'\\delta','δ'}, {'\\Delta','Δ'},
{'\\mu','µ'}, {'\\sigma','σ'}, {'\\pi','π'}, {'\\lambda','λ'}, {'\\theta','θ'},
{'\\left',''}, {'\\right',''},
{'\\qquad',' '}, {'\\quad',' '}, {'\\,',' '}, {'\\;',' '}, {'\\:',' '},
{'\\ ',' '}, {'\\!',''},
{'\\%','%'}, {'\\#','#'}, {'\\&','&'}, {'\\_','_'}, {'\\{','{'}, {'\\}','}'},
{'\\$','$'},
}
-- Literal (non-pattern) string replacement; avoids Lua pattern magic in keys.
local function lit_replace(s, a, b)
local out, i = {}, 1
while true do
local p = s:find(a, i, true)
if not p then out[#out + 1] = s:sub(i); break end
out[#out + 1] = s:sub(i, p - 1)
out[#out + 1] = b
i = p + #a
end
return table.concat(out)
end
-- Strip the outer braces of a "%b{}" capture.
local function grp(b) return b:sub(2, #b - 1) end
-- Map a string to Unicode super/subscript, or nil if any char is unsupported.
local function map_script(txt, map)
local res = {}
for i = 1, #txt do
local c = txt:sub(i, i)
if not map[c] then return nil end
res[#res + 1] = map[c]
end
return table.concat(res)
end
local function convert(s)
-- Text/font wrappers: keep the content, recurse to handle nesting.
for _, cmd in ipairs({'text', 'mathrm', 'mathit', 'mathbf', 'mathbb',
'mathsf', 'mathtt', 'mathcal', 'operatorname',
'textbf', 'textit', 'textrm'}) do
s = s:gsub('\\' .. cmd .. '(%b{})', function(b) return convert(grp(b)) end)
end
-- Fractions -> "num/den" (spaced when either side has spaces).
local function frac(a, b)
local n, d = convert(grp(a)), convert(grp(b))
local sep = (n:find(' ', 1, true) or d:find(' ', 1, true)) and ' / ' or '/'
return n .. sep .. d
end
s = s:gsub('\\frac(%b{})(%b{})', frac)
s = s:gsub('\\dfrac(%b{})(%b{})', frac)
s = s:gsub('\\tfrac(%b{})(%b{})', frac)
s = s:gsub('\\sfrac(%b{})(%b{})', frac)
-- Roots.
s = s:gsub('\\sqrt(%b{})', function(b) return '√(' .. convert(grp(b)) .. ')' end)
-- Single-char scripts first, so the braced fallback (e.g. "_native") below
-- is not re-scanned and mangled into Unicode subscripts.
s = s:gsub('%^([%w])', function(c) return sup[c] or ('^' .. c) end)
s = s:gsub('_([%w])', function(c) return sub[c] or ('_' .. c) end)
-- Braced scripts: Unicode when the content is all digits/signs, else keep
-- a readable "^(...)" / "_..." form.
s = s:gsub('%^(%b{})', function(b)
local inner = convert(grp(b))
return map_script(inner, sup) or ('^(' .. inner .. ')')
end)
s = s:gsub('_(%b{})', function(b)
local inner = convert(grp(b))
return map_script(inner, sub) or ('_' .. inner)
end)
-- Remaining symbols.
for _, pair in ipairs(symbols) do s = lit_replace(s, pair[1], pair[2]) end
return s
end
-- Convert a display equation, preserving its line structure for a code block.
local function convert_display(s)
s = convert(s)
for _, env in ipairs({'cases', 'aligned', 'align', 'array', 'matrix',
'gathered', 'split'}) do
s = lit_replace(s, '\\begin{' .. env .. '}', '')
s = lit_replace(s, '\\end{' .. env .. '}', '')
end
s = lit_replace(s, '\\\\', '\n') -- row break
s = s:gsub('%s*&%s*', ' ') -- column separator -> spacing
local lines = {}
for line in (s .. '\n'):gmatch('(.-)\n') do
line = line:gsub('^%s+', ''):gsub('%s+$', '')
if line ~= '' then lines[#lines + 1] = line end
end
for i = 2, #lines do lines[i] = ' ' .. lines[i] end -- indent continuations
return table.concat(lines, '\n')
end
function Math(el)
if el.mathtype == 'DisplayMath' then
return el -- handled at block level by Para, to emit a code block
end
return pandoc.Str(convert(el.text))
end
-- A paragraph that is solely a display equation becomes a fenced code block.
function Para(el)
local maths, only_math = {}, true
for _, x in ipairs(el.content) do
if x.t == 'Math' and x.mathtype == 'DisplayMath' then
maths[#maths + 1] = x
elseif x.t ~= 'Space' and x.t ~= 'SoftBreak' and x.t ~= 'LineBreak' then
only_math = false
end
end
if #maths == 0 or not only_math then return nil end
local parts = {}
for _, m in ipairs(maths) do parts[#parts + 1] = convert_display(m.text) end
return pandoc.CodeBlock(table.concat(parts, '\n\n'))
end

View File

@@ -7,11 +7,20 @@ sed -i -e 's@\\ctrl@Ctrl@g' _tmp.tex
sed -i -e 's@\\shift@Shift@g' _tmp.tex
sed -i -e 's@\\Alt@Alt@g' _tmp.tex
sed -i -e 's@\\del@Delete@g' _tmp.tex
python3 fa-icons.py ../profiler/src/profiler/IconsFontAwesome6.h _tmp.tex
python3 fa-icons.py ../profiler/src/profiler/IconsFontAwesome7.h _tmp.tex
sed -i -e 's@\\LMB{}~@@g' _tmp.tex
sed -i -e 's@\\MMB{}~@@g' _tmp.tex
sed -i -e 's@\\RMB{}~@@g' _tmp.tex
sed -i -e 's@\\Scroll{}~@@g' _tmp.tex
sed -i -e 's@\\textsigma@σ@g' _tmp.tex
# Resolve \circled{} markers and lstlisting escapeinside (@...@) snippets, which
# pandoc would otherwise emit verbatim or drop, to their Unicode equivalents.
sed -i -e 's|@\\circled{a}@|(a)|g' -e 's|@\\circled{b}@|(b)|g' -e 's|@\\circled{c}@|(c)|g' _tmp.tex
sed -i -e 's|\\circled{a}|(a)|g' -e 's|\\circled{b}|(b)|g' -e 's|\\circled{c}|(c)|g' _tmp.tex
sed -i -e 's|@\\ldots@|…|g' _tmp.tex
# pandoc doens't recognize this lstlisting option and emits it verbatim
sed -i -e 's|,\?escapeinside={}{}||g' _tmp.tex
sed -i -e 's@\\nameref{quicklook}@A quick look at Tracy Profiler@g' _tmp.tex
sed -i -e 's@\\nameref{firststeps}@First steps@g' _tmp.tex
@@ -26,7 +35,10 @@ sed -i -e 's@\\nameref{configurationfiles}@Configuration files@g' _tmp.tex
awk -f bclogo2quote.awk _tmp.tex > _tmp_quoted.tex
mv _tmp_quoted.tex _tmp.tex
pandoc --wrap=none --reference-location=block --number-sections -L filter.lua -s _tmp.tex -o tracy.md
pandoc --wrap=none --reference-location=block --number-sections -L filter.lua -t 'markdown-simple_tables-multiline_tables-grid_tables+pipe_tables' -s _tmp.tex -o tracy.md
awk -f tablecaption.awk tracy.md > _tmp_caption.md
mv _tmp_caption.md tracy.md
sed -i -e 's/^> \*\*IMPORTANT:\([^*]*\)\*\*/> [!IMPORTANT]\
> **\1**/' tracy.md
@@ -37,6 +49,6 @@ sed -i -e 's/^> \*\*CAUTION:\([^*]*\)\*\*/> [!CAUTION]\
sed -i -e 's/^> \*\*NOTE:\([^*]*\)\*\*/> [!NOTE]\
> **\1**/' tracy.md
python3 icon-explain.py ../profiler/src/profiler/IconsFontAwesome6.h tracy.md
python3 icon-explain.py ../profiler/src/profiler/IconsFontAwesome7.h tracy.md
rm -f _tmp.tex

16
manual/tablecaption.awk Normal file
View File

@@ -0,0 +1,16 @@
# Pandoc emits table captions as a line beginning with ": ", which GitHub
# renders literally instead of as a caption. Strip the marker and italicize
# the caption instead. Captions may span several physical lines when they
# contain a hard line break (a trailing backslash). Underscores are used for
# the emphasis so captions that already contain "*...*" markup are left intact.
!incap && /^: / {
incap = 1
$0 = "_" substr($0, 3)
}
incap && !/\\$/ {
print $0 "_"
incap = 0
next
}
incap { print; next }
{ print }

Some files were not shown because too many files have changed in this diff Show More