sbox-public Documentation

S&box is a modern game engine, built on Valve's Source 2 and the latest .NET technology, it provides a modern intuitive editor for creating games

6,489 stars C# C# 1 file ยท ~16,103 tokens #gamedev#sbox#source2
RAW Doc

Technical Documentation: Facepunch/sbox-public

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

s&box

s&box is a modern game engine, built on Valve's Source 2 and the latest .NET technology, it provides a modern intuitive editor for creating games.

If your goal is to create games using s&box, please start with getting started guide.
This repository is for building the engine from source for those who want to contribute to the development of the engine.

Getting the engine

Steam

You can download and install the s&box editor directly from Steam.

Compiling from source

This repository contains the C# engine, editor, tooling and game content. The native Source 2 core is
distributed as prebuilt binaries that setup downloads for your platform, so no C++ toolchain is needed.

Platform Setup Notes
Windows 10 / 11 (x64) Setup.bat
Linux (x64) ./Setup.sh Binaries target the Steam Linux Runtime, most distros should work.
macOS (Apple Silicon) ./Setup.sh Intel Macs are not supported.

Prerequisites

Setup

bash
# Clone the repo
git clone https://github.com/Facepunch/sbox-public.git
cd sbox-public

# Windows
Setup.bat

# Linux / macOS
./Setup.sh

Once setup completes, the game (sbox) and editor (sbox-dev.exe) run from the game folder.

Staying up to date

Pulling on Git will run a hook that fetches any new native binaries or content that changed and regenerates the interop bindings. Build the C# code from your IDE as usual, or rerun Setup.bat for a full incremental rebuild including shaders and content.

Contributing

If you would like to contribute to the engine, please see the contributing guide.

If you want to report bugs or request new features, see sbox-issues.

Documentation

Full documentation, tutorials, and API references are available at sbox.game/dev/.

License

The s&box engine source code is licensed under the MIT License.

Certain native binaries in game/bin are not covered by the MIT license. These binaries are distributed under the s&box EULA. You must agree to the terms of the EULA to use them.

This project includes third-party components that are separately licensed.
Those components are not covered by the MIT license above and remain subject
to their original licenses as indicated in game/thirdpartylegalnotices.


Overview (`engine/Sandbox.CodeUpgrader/Readme.md`)

Analyzer

Analyzers create diagostrics. The analyzers look at the code for stuff and if it finds
it, it creates a diagnostic.

Diagnostic

A diagnostic is one of those CS234 things you get in the output in your visual
studio console. We want them to all have unique numbers so we define them in
a big static class called Diagnostics, and reference them from there.

Fixer

The fixers respond to the diagnostics, and use roslyn to convert one peice of
code to a different peice of code.

Tests

Each Analyzer and Fixer has its own tests. To make things simpler the tests
are in the class itself.

The tests aren't automatically collected, you need to add them to the test
project ( in UpgraderTests.cs )

Test markup

In the test code you're going to see this weird [| and |] markup.

This is telling the tester that we expect a diagnostic between these two tokens.

For example if we have this code:

text
class [|Type1|] { }

We're telling the test that we're expecting a diagnostic to trigger for the
name Type1 for some reason. If it does create a single diagnostic for that span
then the test will pass.

If you do this

text
class Type1 { }

and no diagnostics are created then it will pass.

more: https://www.meziantou.net/how-to-test-a-roslyn-analyzer.htm#marker-syntax


Overview (`engine/Sandbox.SceneRenderer/README.md`)

Sandbox.SceneRenderer

A C# scene renderer for s&box, intended to replace the native scenesystem's world rendering.
It manages render objects, visibility, lighting and frame execution while using the engine's existing
materials, shaders and graphics backend.

It also holds the layers of native's rendering pipeline (Native): native sets each view up and
this adds its layers, for every camera that isn't rendered here. See the
render pipeline doc.

The renderer is experimental. To use it for GameObject scenes, enable r_managed_scene 1.
It is disabled by default. SceneLab runs the managed and native renderers
against the same scenes for visual comparison and performance testing.

This README covers the project structure and development workflow. The
renderer reference contains the implementation details,
native equivalents, performance investigations and debugging history.

What it renders

  • Static and skinned meshes, with LOD selection, instancing, morphs and model deformers.
  • Opaque, translucent and faded geometry, decals, glass, viewmodels and screen overlays.
  • Clustered point and spot lights; directional, spot and point shadows; contact shadows.
  • Environment maps, baked lighting, light probe volumes, 2D and 3D skies, and gradient, cubemap and volumetric fog.
  • Custom objects such as particles, sprites, terrain, clutter, debug geometry and world panels.
  • Camera command lists, post processing, auto exposure and UI through the existing camera stages.
  • Render-to-texture targets, MSAA, tools materials and debug views.

The GameObject bridge mirrors scene changes into the renderer, including material overrides,
body groups, tags, per-object attributes and camera settings.

Architecture

RenderWorld owns the scene data. RenderView describes a camera, and RenderSystem turns the visible
part of a world into a frame. Features prepare and draw different kinds of objects; layers specify the
steps of the frame. FrameRecorder decides which layers are needed and records their work for submission.

Rendering uses a dedicated native IRenderContext, rather than native ISceneSystem, ISceneView or
ISceneLayer objects. The renderer still relies on shared engine services, including the material
system, shader code, shadow-map policy and skinning vertex cache.

Source layout

Folder Responsibility
World Scene foundations: RenderWorld, RenderObject and the CustomObject extension point.
World/Objects Meshes, lights, environment-map objects and decals, plus their RenderMesh data.
World/Environment Scene lighting, light probe volumes, fog settings and the 3D skybox.
Culling The dynamic spatial tree, frustum tests and screen-size culling.
Pipeline Cameras, frames, targets, collection, preparation and command recording.
Pipeline/Features RenderFeature, mesh preparation and drawing, and clustered light/probe/decal binning.
Pipeline/Layers Render passes and their resource dependencies.
Pipeline/Shadows Integration with the engine's shared ShadowMapper.
Pipeline/Fog Volumetric fog lighting, temporal accumulation and integration.
Gpu Native rendering access, GPU uploads, transform buffers and shader constants.
Bridge Synchronization with GameObject scenes and execution of existing camera stages.
Diagnostics Frame comparison, rendering statistics and profiler instrumentation.
Native Native's rendering pipeline: the layers of its standard and 3D skybox views (NativeRenderPipeline).

Folders describe responsibilities; they do not all introduce namespaces. Public types use
Sandbox.SceneRenderer. Internal subsystems also use .Features, .Culling, .Shadows, .Gpu,
.Bridge and .Native; the first four are imported within the project by Assembly.cs.

Shared math belongs in Sandbox.System. In particular,
Matrix3x4 lives alongside Matrix and represents the native affine
layout used for bones and transform buffers.

How a frame runs

RenderSystem.Render( world, view, target ) renders into a ViewTarget. The internal swap-chain
overload is used by the engine and SceneLab.

  1. Collect: query the spatial tree, test exact bounds and screen size, then group visible objects by feature.
  2. Prepare: choose LODs, sort draw runs, pack lights and write transform data. The shadow system requests
    additional views, which are culled and prepared in the same way.
  3. Setup: create or resolve GPU resources, upload constants and buffers, and dispatch feature setup work.
  4. Draw: record the required layers and submit them in order, resolving or copying the final output as needed.

Collect and Prepare are CPU-only. This keeps them testable without booting the native engine.

The layer order is defined in RenderSystem.Layers.cs.
Layers declare the resources they read and write through FrameResources. Planning walks those
dependencies backwards and drops producers whose output is unused. IsNeeded handles whether a layer
can do useful work for the current frame.

Large frames record work in parallel. Each recording segment has its own native context, created on
the thread that records it. Shadow culling is also parallelized. Camera stages and custom-object work
that requires the main thread stay there. Small frames avoid the overhead of parallel recording.
With r_managed_async_compute (enabled by default), depth-chain, contact-shadow and AO work can overlap
shadow-map rendering. SSR automatically starts on graphics once Hi-Z is ready, alongside AO on compute.

Declare each pass's inputs, outputs and compute capability. The pipeline itself stays a plain list:

csharp
// On the AO pass:
public override FrameResources Reads( RenderFrame frame ) => FrameResources.DepthChain | FrameResources.Normals;
public override FrameResources Writes => FrameResources.AmbientOcclusion;
public override bool SupportsAsyncCompute => true;

// In the pipeline, in serial execution order:
new DepthChainLayer(),
new EarlyStageLayer( Stage.AfterDepthPrepass, PostProcessQueue.AsyncCompute ),
contactShadows,
new EarlyStageLayer( Stage.AfterDepthPrepass, PostProcessQueue.Graphics ),
new ShadowLayer(),
new ShadowsReadyLayer(),

FrameRecorder finds consecutive compute-capable passes and the following independent graphics work. It
splits compute submissions at the results graphics needs and inserts waits for read/write, write/read and
write/write conflicts. Read-only sharing needs no wait. Graphics producers submit before the next compute
batch's handoff; outstanding compute joins before that handoff, another frame or untracked work. No groups,
named pass dependencies or manual semaphore wiring are required in the pipeline.
Reusing an earlier compute pass's input or output starts another batch, so graphics preparation cannot clear it too early.

Reads defaults to FrameResources.Untracked: an undeclared pass is a conservative scheduling boundary.
To allow overlap, override it with all resources the pass reads (None for no inputs), and declare its
outputs in Writes. Include Untracked alongside known inputs for arbitrary camera hooks or custom drawing.
These declarations also drive output culling; HasSideEffects keeps work such as the depth prepass even
when no later pass explicitly reads its output. This does not inspect shader bindings or allocate resources.

Each queue preserves list order. Disabled/unsupported async compute and windows with no independent graphics
work stay serial. CPU worker thresholds do not control GPU overlap. Graphics-only preparation and result
transitions remain in HandOff and TakeBack; preparation must only initialize outputs and transition inputs
that already exist before the batch. Early camera effects stay at their normal stage on the serial path.

Connections to the rest of the engine

  • ManagedScene owns the convars, scene-change notifications
    and shutdown integration. The app system registers this assembly with the engine at startup
    (EngineHooks.cs).
  • ShadowMapper supplies shared shadow allocation, caching
    and time-slicing policy for both renderers.
  • Graphics.IFrameView lets camera effects and
    custom drawing operate on either renderer. Use the Graphics abstraction rather than assuming a
    native scene view or layer exists.
  • RenderTools.def exposes native helpers implemented in
    rendertools.cpp.
  • Gpu/SharedPasses.cs holds the depth chain, quarter depth and bloom and refraction
    targets, which this renderer and native's pipeline both use.

Building and testing

Run these commands from the repository root. Close SceneLab before building because it locks the
assemblies in game/bin/managed. See the build guide for
initial setup and native build options.

powershell
# Build managed code and regenerate bindings, using the existing native build.
dotnet run --project engine/Tools/SboxBuild/SboxBuild.csproj -- build --skip-native

# Run the renderer's unit tests after building.
dotnet test -c Release --no-build engine/Tests/Sandbox.Test.Unit --filter "FullyQualifiedName~SceneRendererTests"

The unit tests cover culling, object storage, transforms,
GPU layouts, light packing, layer selection and allocation behavior. Tests that prepare frames use
[DoNotParallelize] because ShadowMapper and Application.FrameCount contain shared static state.

Visual changes require a parity sweep and inspection of the captured images. A passing comparison
alone cannot show that a feature rendered correctly: both renderers might have drawn nothing or used
the same missing asset. A new feature needs a SceneLab preset and a matching native reference.
Disable the feature once to confirm the comparison detects its absence.

Visual comparison

With a game loaded, run these commands in the console:

text
r_managed_scene 1
r_managed_scene_compare 120

The comparison waits 120 camera renders, then captures the next camera through both renderers.
Results go to the console and images to game/screenshots/managed_scene/. Allow the scene to finish
loading before comparing.

In SceneLab, use View > Compare With Native for the current scene. To run a preset sweep, launch
the built-in runner directly from game/:

text
bin/managed/scenelab.exe -parity
bin/managed/scenelab.exe -parity "Sun Shadows;Reflections" -zoom 0.3

The sweep writes its results and captures to game/screenshots/scenelab/parity/, and exits with code 1
if any comparison fails. Use -parity-out <folder> to choose another output location.

  • <scene>.png: the managed frame.
  • <scene>.native.png: the native frame.
  • <scene>.diff.png: the managed frame dimmed, with differences highlighted in red.

Effects with shared or temporal state need isolated comparisons. Use -parity -isolated for those checks;
AO and SSR presets already request isolation. Test scene changes as well as initial rendering, and
check representative games when changing the bridge.

Changes to the shared shadow mapper also require a native-to-native comparison. Run
bin/managed/scenelab.exe -parity -save-native before from game/, make the change, then run
bin/managed/scenelab.exe -parity -compare-native before.
For deterministic presets, those frames should be byte-identical.

Performance and frame inspection

From game/, run:

powershell
bin/managed/scenelab.exe -scene "All Shadows"
bin/managed/scenelab.exe -benchmark "10k Boxes;50k Mixed" -novsync

Benchmark results go to game/screenshots/scenelab/benchmark.md. Warm up maps before measuring so asset
loading, JIT compilation and shader compilation do not dominate the result. Include scene switching
when testing resource lifetime.

Use overlay_scene_plan 1 in a game, or View > Frame Plan in SceneLab, to see active layers,
resource dependencies, recording segments and timings. Level 2 also shows skipped layers.
Tracy zones are defined in Diagnostics/Zones.cs; they cover collection,
preparation, setup, recording, worker waits and submission. SceneLab's -serialrecording switch helps
compare serial and parallel execution.

Development conventions

Read the relevant section of the renderer reference before
changing a subsystem. When porting behavior, cite the native function or file in the code comment so
the implementation can be checked against its source.

Allocation and threading

  • Steady-state per-frame work must not allocate. Reuse buffers and pool per-view state by stable
    identity, such as a shadow map and face. PipelineTests.CollectAndPrepareDoNotAllocate checks the
    collection and preparation path, including shadows.
  • Avoid LINQ, interface-based enumeration, iterators, allocating params calls, string formatting,
    capturing lambdas and repeatedly created delegates in hot paths.
  • Resolve textures, bindless indices and other native resources during Setup, not Collect or Prepare.
  • Worker jobs read prepared state and write only their own context, attributes and view-local lists.
    Warm mutable caches before dispatching jobs; do not lazily create materials on a recording thread.
  • Mark work that needs Graphics or other main-thread-only engine code through
    RenderFeature.CanRecordOffMainThread.
  • Each pass binds its own targets, viewport, depth range and view constants through BeginPass or
    BindFrame. Use SetPassCombo for pass-local shader choices. A pass must not depend on state left
    by the previous pass.

GPU resources and interoperability

Keep native rendering calls in Gpu/RenderContext. Add new native helpers through rendertools and
the .def bindings, then regenerate with tools/BuildBindings.bat. Never hand-edit generated interop.
Native changes require a native build; shader changes require shader compilation before parity testing.

Dispose owned GPU resources before native shutdown. Cached materials returned by Material.FromShader
are shared; only destroy materials created and owned by the caller. Context state changes must go
through RenderContext helpers so cached material bindings are invalidated correctly.

GPU structs must match the native and shader layouts, including packing and matrix conventions.
Check those layouts when updating native dependencies. Skinning uses the engine's shared vertex cache;
morph and vertex-cache preparation needed by this renderer must complete before its draw submission.

C# style

Use the engine's Matrix, Vector2, Vector3, Vector4 and Rotation types. They already wrap
System.Numerics; direct Numerics usage needs a concrete reason, such as an interface that requires it.
Avoid shorthand aliases such as V3, V4 and M4. Remember that Vector3 == is approximate; use
Equals when cache invalidation or validation requires exact equality.

Follow the repository's .editorconfig: tabs, spaces inside parentheses, _camelCase instance fields
and s_camelCase mutable static fields. Prefer collection expressions and target-typed new where
appropriate, while retaining preallocated capacity for scratch arrays. Document public types and
members (CS1591 is an error here), and give internal types a summary explaining their role.

Extending the renderer

Add an object type

  1. Derive from RenderObject in World/Objects/. Override SizeCulled for objects that should stay out
    of the size-culling tree, as lights and probes do.
  2. Add its preparation and drawing in Pipeline/Features/, using the RenderFeature contract.
    Include shadow support and the main-thread recording requirement where applicable.
  3. Register the feature in RenderSystem. Registration order matters; light binning runs first.
  4. Add bridge synchronization where needed, a SceneLab preset, and its native equivalent in NativeScene.

Add a layer

Add the layer in Pipeline/Layers/ and register it in RenderSystem.Layers.cs at the appropriate point.
Declare resource Reads and Writes; let dependency planning decide whether a producer's output is
needed rather than duplicating all reader conditions in IsNeeded.

Mesh layers select their run lists and shader mode. Additional run lists belong in MeshRuns and the
classifier in MeshRenderFeature.Lists.cs, with a corresponding ClassifierTests case.

Add shader inputs or bridge state

Use native view setup and light-binning code as the reference for shader inputs. Missing constants
often produce incorrect pixels without an error. ViewEnvironment supplies shared environment inputs;
each pass must still supply constants for its own target and viewport.

Bridge changes must preserve native object flags, layer membership, material overrides and camera
attributes. Read native draw state rather than deriving it again. New state setters must notify the
mirror, including changes made indirectly by native code. Verify the scene both before and after a change.

Debugging and remaining work

For incorrect frames, start with pass constants, resource dependencies, object flags and shader modes.
Use debug views to isolate shading terms and inspect the depth chain for depth-dependent effects.
Check that the assets exist and that both renderers have comparable temporal history.

The renderer reference records detailed investigations into
color space, shadow behavior, skinning, material binding, bridge synchronization and parallel recording.

Remaining work includes moving more camera effects into this renderer, threading main-view collection
and preparation, and reducing native per-draw overhead. Known gaps include morphs on meshes without a
skeleton, glass shadow dithering differences, layer-specific attribute overrides and DDGI parity with
baked test data. An intermittent native shutdown crash has also been observed in 256-light scenes;
it still needs a native stack trace.


Overview (`engine/Mounting/Sandbox.Mounting.Source1/README.md`)

Source 1 mount

The engine's gmod mount reads supported models, materials, textures and sounds
from GMA/VPK archives and loose files inside the Garry's Mod installation.

Registration and archives

The installation is inventoried in one filesystem pass, including nested addon,
gamemode content, download and cache folders. Directory links/reparse points are
skipped to avoid cycles or leaving the installation. Unreadable directories are
skipped with a warning while other directories continue mounting. Files directly
in the installation root are included; top-level materials, models, sound and
scripts directories retain their canonical prefixes.

Archives precede loose files. Archives inside garrysmod take priority over
other installation folders, with GMAs before VPKs in each priority group and
case-insensitive path sorting for stable first-match resolution. Loose content
keeps its indexed paths without unprefixed addon or gamemode aliases.
Shadowed loose files are logged at trace verbosity; the first indexed source
continues to win.
Model registration uses only companion filenames; models without a VVD/DX90 VTX
pair or a PHY companion remain indexed for dependencies but are not advertised.
Texture headers register the supported individual frames and animation arrays.

GMA versions 1-3 share the VPK reader's leased random-access stream implementation.
Indexing reads directories, not whole payloads; assets load lazily without
extracting archives. Invalid paths, duplicate GMA entries and out-of-bounds data
are rejected. Invalid or unreadable archives are logged as warnings without
marking otherwise valid mounted content as erroneous. Split VPK chunks with a
matching directory archive are skipped, and alternate names for the same VPK
directory are deduplicated.

Only supported model, material, texture and sound resources are registered;
Lua is not executed and BSP/map import is not included. Local Workshop GMA files
inside the installation are supported, but external Steam Workshop folders and
automatic Workshop downloads are not discovered. Remount after downloading or
changing content.

Material dependency resolution

VMT parameters target Windows PC DX90/PS2.0b. Shader fallback blocks such as
>=dx90_20b and the $DX90_20B condition are supported. Both combine soldier
skins keep their base texture and self-illumination mask from those blocks.

Loose models resolve their MDL material paths within their enclosing content root
before falling back to ordinary mounted content. VMT patch includes, relative
KeyValues directives, textures and HDR environment maps keep that same root.
For example, addons/example/models/ resolves against
addons/example/materials/ without adding global aliases. Full indexed resource
paths keep separate addons' materials and texture caches distinct. VMTs retain
their actual indexed entry paths rather than receiving an extra materials/
prefix. Reimport existing models to replace old error-material bindings.

There is no arbitrary basename matching or cross-installation dependency
fallback. Genuinely missing assets remain reported rather than receiving
unrelated replacement materials.


Overview (`engine/Sandbox.Tools/Mcp/README.md`)

MCP tools and sbox-cli

For agent-facing usage, see the sbox-cli skill,
which ships beside the executable and is printed by sbox-cli --skill sbox-cli. Agents find it
through a small bootstrap skill (bootstrap/sbox,
also published as Facepunch/sbox-skills), installed for the user. The editor offers it once when
it finds Claude Code or Codex; Editor Preferences โ€บ AI Agents and sbox-cli --skill-install manage it.

game/bin/managed/sbox-cli.exe is a .NET 10 console application that keeps one MCP connection
open over stdin/stdout, or makes a single call with --call. It has no engine dependencies of its own. Editors expose a local
named pipe by default, controlled by Project Settings > AI > Enable MCP Communication;
games expose one when launched with -mcp. The pipe is restricted to the current
OS user and named using the process ID and start time. On Windows the CLI also verifies the
server's actual PID. Multiple editors and games can therefore run independently without TCP ports.

The preference is stored in the .sbproj and shared with collaborators. Saving MCP as disabled
immediately closes connected transports and cancels pending work; already running scripts must
cooperate with cancellation.

powershell
# Discover running instances that support the new transport.
game/bin/managed/sbox-cli.exe --list

# One call against the editor for the project in the current directory, then exit.
game/bin/managed/sbox-cli.exe --call editor_status
game/bin/managed/sbox-cli.exe --call search_tools '{"query":"scene"}'

# Attach; closing the CLI leaves the process running. Takes a PID, a project, or nothing for
# the project containing the current directory (or the only instance, outside any project).
game/bin/managed/sbox-cli.exe --connect
game/bin/managed/sbox-cli.exe --connect 12345

# Attach to the project's editor if it's open, otherwise launch and own one.
game/bin/managed/sbox-cli.exe --project C:/projects/my-game

# Create a project from a template, like the launcher does. Prints the new .sbproj path.
game/bin/managed/sbox-cli.exe --templates
game/bin/managed/sbox-cli.exe --new "My Game" --template game.minimal --in C:/projects

# Launch and own a game.
game/bin/managed/sbox-cli.exe --game C:/projects/my-game/my-game.sbproj
game/bin/managed/sbox-cli.exe --game facepunch.sandbox

A project is a .sbproj or the directory containing it. Inside a project, the CLI only ever
attaches to that project's instance. Launches open normal visible windows. --engine <directory> overrides the game installation
inferred from the CLI's location. Owned launches currently require Windows. An existing process
must be running an engine build with the pipe support; restart older editors after rebuilding.

Stdio protocol and session files

Configure an MCP client to run the CLI with one connection mode and keep that process open.
Do not start a new CLI for every request. Each request/response is one UTF-8 JSON-RPC line:

jsonl
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"instance_status","arguments":{}}}
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"search_tools","arguments":{"query":"scene"}}}

Wait for the initialization response before sending subsequent messages. Its instructions
explain tool discovery, conventions and the session exchange directory. _meta contains pid,
kind, project and exchangeDirectory for programs. The unique directory is created under
%TEMP%/sbox-cli/; use it for scripts, screenshots and other files shared with the instance.
It is retained after disconnect so outputs remain usable. Copy wanted outputs to the project,
then remove the session directory when finished. Discovery alone does not create directories.

Stdout contains only protocol messages in connection modes. Startup messages and diagnostics
go to stderr. Requests are limited to 8 MiB, responses to 64 MiB, with at most 32 outstanding
requests per CLI. Each connection executes requests in order; separate connections share the
engine main thread. Tools, discovery and binding run on that thread in game scope.

C# scripts

Use search_tools with csharp to find run_csharp, then call it directly or through call_tool:

jsonl
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"run_csharp","arguments":{"code":"return Game.ActiveScene?.Name;"}}}

Supply a C# async method body in code, or an absolute UTF-8 file path (not both).
System, System.Linq, System.Threading, System.Threading.Tasks, Sandbox, and the game
global namespace are imported. await, return, exchangeDirectory and cancellationToken
are available. References come from loaded assemblies, including the active addon assemblies;
editor types can be fully qualified in an editor. Compile diagnostics use script line numbers.
This is trusted local automation with full process privileges, and mutations have no automatic
undo. The protocol, transport, registry, session state and built-in tool classes are internal.
Built-in tool methods are also internal and described only in a private MCP registry; never add
[Expose], [Library] or [ActionGraphInclude] to them. Game code must not reach them through
its TypeLibrary.
Only the tool declaration attributes, hints and McpResult builders are public for addon authors.

Lifetime

Closing stdin, losing the pipe, or Ctrl+C shuts down the bridge. An attached instance
(--connect, --call, or --project on an editor that was already open) is never terminated.
For an instance it launched, the CLI first requests a graceful quit,
waits up to two seconds, then closes its Windows job to terminate the owned process tree.
The instance is assigned to the job atomically during creation, before running; the job also closes if the
CLI crashes or is forcibly killed. Engine processes do not inherit the CLI's stdio handles.

The defaults are --startup-timeout 180, --idle-timeout 600, and --request-timeout 300
(seconds, configurable from 1 to 86400). Idle timeouts apply only without outstanding requests;
logs and other unsolicited output do not renew them. A running request has its own deadline.
On timeout the connection closes. Queued calls are cancelled on disconnect or after a 30-second
main-thread pickup timeout. Running code cancels cooperatively; use cancellationToken in
long scripts. A running mutation may have completed before disconnection, so inspect state
before retrying. An attached instance is never killed to stop a stuck script.

Tool discovery

A tool is a static method. There is no registration - discovery goes through the editor or game TypeLibrary,
so tools in addons and hotloaded assemblies appear and disappear as code compiles. Write the
method, save, and an agent can call it.

The registry and protocol live in engine/Sandbox.Engine/Systems/Mcp/.
The established Editor.Mcp attribute/result namespace remains available for source and binary
compatibility, including game tools. Editor-specific tools stay in Sandbox.Tools and editor
addons.

Adding a tool

csharp
namespace Editor.Mcp;

[McpToolset( "car", "Everything about the cars in the scene" )]
public static class CarTools
{
	/// <summary>
	/// Find cars by name. Returns each car's id - pass one to get_car for details.
	/// </summary>
	/// <param name="query">Case insensitive substring of the car's name. Empty matches everything.</param>
	/// <param name="limit">How many results to return. Default 20, max 100.</param>
	[McpTool( "find_cars" )]
	public static object FindCars( string query = "", int limit = 20 )
	{
		...
		return new { Total = total, Cars = cars };
	}
}

That's everything. In addon code the XML summary becomes the tool description and `` docs
become the parameter descriptions (codegen turns them into attributes). In engine assemblies there
is no codegen - put [Description( ... )] on the method and each parameter instead.

The description is the API. Agents pick tools by reading it, so say what the tool returns, what to
call next, and anything surprising. Cross reference other tools by name - "as returned by
find_cars" tells an agent exactly where a value comes from.

  • [McpTool] - marks the method. Pass a name ("find_cars") or one gets derived from the method
    name in snake_case (FindCars โ†’ find_cars). Names are public API - agents' workflows break
    when they change.
  • [McpToolset] on the class groups its tools under a name and description, browsable through
    list_toolsets. Without it the name derives from the class name (CarTools โ†’ car).

Tools don't go in the client's static tool list - agents find them live through search_tools and
invoke them through call_tool, which is what keeps the registry fresh across hotloads. (The
handful of built in entry points are listed via the internal [McpListed], and that's deliberate -
a list a client fetched once must never go stale.)

Parameters

Parameters become the tool's json schema. A parameter with a default value is optional; without
one it's required.

C# type What the agent sends
string A string. Anything else gets stringified, so this is the most forgiving type.
bool true / false
int, long, byte... An integer
float, double, decimal A number
enums The name as a string, case insensitive. The schema lists every value.
Vector2/3/4, Vector2Int/3Int, Angles, Rotation A comma string - "100,0,50", "pitch,yaw,roll"
Guid, DateTime, TimeSpan, char A string
T[], List<T> An array of T
Dictionary<string, T> An object with T values
JsonObject, JsonArray, JsonNode Raw json, taken as sent
A class or struct you wrote An object - see below

Binding is deliberately tolerant, because agents make predictable mistakes: argument names match
case insensitively, values that arrive json-encoded inside a string get unwrapped, numbers arrive
as strings, enum names in the wrong case still count. Unknown argument names fail loudly - silently
ignoring a typo'd argument does the wrong thing invisibly.

Custom classes and structs

A plain data type works as a parameter (or inside one - arrays, lists, nested properties). The
schema is generated from its public settable properties:

csharp
/// <summary>
/// One waypoint on a patrol route.
/// </summary>
public class Waypoint
{
	/// <summary>Where to stand, as 'x,y,z'.</summary>
	public required Vector3 Position { get; set; }

	/// <summary>Seconds to wait before moving on.</summary>
	public float Wait { get; set; }
}

[McpTool( "set_patrol" )]
public static object SetPatrol( string id, Waypoint[] waypoints ) { ... }
  • Every public property with a public setter becomes a schema property, advertised in camelCase
    (Position โ†’ position). Binding is case insensitive so both spellings work.
  • The C# required keyword makes the property required in the schema, and the serializer enforces
    it - a call missing position fails with an error saying so.
  • Describe properties the same way as everything else - XML summaries in addon code,
    [Description] in engine code.
  • [JsonPropertyName] and [JsonIgnore] are respected.

Engine and framework types don't get their properties reflected - most serialize through custom
converters (Vector3 is the string "x,y,z", not {x, y, z}), so a guessed shape would be wrong.
They keep an open schema; prefer the types in the table above.

Return values

Return whatever is most honest, it gets shaped into an MCP result:

Return What the agent gets
string The text as is
Any object Serialized to json - anonymous objects are the usual shape
GameObject / Component, at any depth A compact reference - { "type": "PointLight", "id": ..., "gameObject": ... } - never a serialized scene subtree. The id chains into get_game_object.
Resource (Model, Material, any GameResource) Its resource path - "models/chair.vmdl" - which chains into asset_info and friends. That's the engine's own serialization; no special handling.
Bitmap A png image
McpResult Full control - compose text and image blocks, e.g. McpResult.Image( thumb ).WithText( path )
null / void "ok"
Task<T> / async Awaited, then shaped as above

Shape returns for a reader, not a serializer: return counts alongside truncated lists
(new { Total, Showing, Assets }), format dates as strings, and leave out fields nobody asked for.

Declare a data class as the return type instead of object and the tool advertises an
outputSchema generated from its properties, and every result carries structuredContent the
client can bind to it. Anonymous returns stay plain text - use a class when the shape is stable
and worth promising.

Errors

Throw. The exception's message reaches the agent as an error result it will read and adapt to, so
write the message for that reader: say what was wrong and what to do instead.

csharp
throw new Exception( $"No car named '{name}'. Find cars with find_cars." );

Everything from tool lookup onward goes back in-band - unknown tools, arguments that don't bind,
failures thrown mid-tool - written with the same care: suggestions for near-miss names, the tool's
real parameter list. The agent reads the error and self corrects. You get all of that for free.

Threading and lifetime

Tools run on the editor main thread - touch scenes, assets and widgets freely, no marshalling. If
the main thread doesn't pick the call up within 30 seconds (modal dialog, long operation) the agent
is told the editor is blocked; the queued call is cancelled and will not run later.

Tools can be async. The main thread invokes the method, the result is awaited off it - so do slow
work (network, compiles) after your last touch of engine state, or hop back with MainThread.

Conventions

Registry-wide rules live in one place - the Instructions string in McpProtocol.cs, which every
client injects into its agent's context at connect. Add new rules there instead of repeating them
per tool. The current ones:

  • Paging is limit and offset. Say the default and max in the parameter description.
  • Vectors and angles are comma strings - 'x,y,z', 'pitch,yaw,roll'.
  • Coordinates are Source engine convention: one unit is one inch, +x forward, +y left, +z up,
    angles in degrees.
  • Game objects and components are identified by guid, assets by the relative path asset_search
    returns.

Trying it out

From the project directory, sbox-cli --call search_tools '{"query":"car"}' finds your tool and
sbox-cli --call find_cars calls it. For an MCP client, configure a stdio server with the full
path to sbox-cli.exe and ["--connect"] (or ["--project"]) as arguments; the client's working
directory selects the project.

Then ask it to search_tools for yours and call it. read_console shows what the editor logged,
which is also where your tool's errors and prints end up.


Overview (`engine/Tools/InteropGen/docs/README.md`)

InteropGen

InteropGen generates the C++โ†”C# bindings for the engine. It reads .def files (a small DSL
describing the classes and structs that cross the native/managed boundary) and writes, for each def:

  • a managed file (cs directive) - typed C# wrappers, [UnmanagedCallersOnly] exports and the
    NativeInterop bootstrap
  • a native header (hpp directive) - declarations of the managed classes native can call
  • a native source file (cpp directive) - exported thunks, import function pointers and the
    igen_* initializer

The generated output is not committed (see .gitignore - Interop.*.cs / interop.*); the
build regenerates it, and files whose content didn't change are left untouched.

Documentation

File Contents
def-files.md The .def file format: top-level directives, includes, inherit/skipall
classes.md Declaring classes, structs, enums; functions, variables, attributes
types.md Argument types, marshalling, flags (out, ref, CastTo[...], ...)

How it runs

The build (Tools/SboxBuild) calls Facepunch.InteropGen.Program.ProcessManifest( "engine" ) from
the repo root. That reads engine/manifest.def - a plain list of .def files - and processes each
listed def in parallel. Files whose content didn't change are not rewritten.

Project layout

text
Program.cs        Entry point - reads the manifest, processes each def
Logger.cs         Per-thread buffered console output so parallel defs don't interleave
Parsers/          Line-oriented parsers for the .def DSL (global โ†’ class โ†’ inline body)
Definition/       The model: Definition, Class, Function, Variable, Struct
Arguments/        One Arg type per marshallable type - each knows its C#/C++ types and conversions
Pipeline/         InteropPipeline (parse โ†’ resolve โ†’ sort โ†’ mangle โ†’ hash), TypeResolver, Mangler
Writer/           The three emitters (ManagedWriter, NativeWriter, NativeHeaderWriter) and helpers

The pipeline (Pipeline/InteropPipeline.cs) turns a def file into a resolved Definition:

  1. Parse - GlobalParser reads the file (and its includes) line by line
  2. Resolve - TypeResolver links base classes, pulls inherited functions down, and converts
    every unknown type name into a concrete Arg
  3. Sort - structs and classes are ordered by name so output is deterministic
  4. Mangle - Mangler gives every function/variable a unique C-safe export name
  5. Hash - the def text is hashed; both sides verify it at runtime so a stale binary fails fast

Invariants - read before changing anything

The native and managed sides are built from the same generated files, and the runtime verifies the
def hash when the tables are exchanged - so a stale native/managed pairing fails fast. When
changing the generator:

  • Never change the hash computation, the name mangling or the sort order unless both sides will
    be regenerated and rebuilt together (they normally are - the build runs the generator).
  • A pure refactor should be verified by snapshot-diffing the output, since the generated files
    aren't in git: run the generator before your change, hash every output, run it again after, and
    compare.
text
# from the repo root, with a tiny console app calling Program.ProcessManifest("engine"):
dotnet run                          # before the change
<hash engine/Sandbox.*/Interop.*.cs and src/**/interop.*>   # snapshot A
dotnet run                          # after the change
<hash again>                        # snapshot B - must equal A for a pure refactor

Two ordering details that are easy to break:

  • The native export table (casts, functions, variable get/set) is index-matched between C++ and C#.
    Both emitters drive it from Writer/NativeExportTable.cs - the single source of truth for slot
    order. Don't enumerate slots anywhere else.
  • Mangled names de-duplicate collisions with numeric suffixes in encounter order, so reordering
    classes or members in a def can rename exports. That's fine - both sides regenerate together -
    but it's why output must always be regenerated as a pair.

Overview (`game/unittest/addons/dmmod/readme.md`)

This addon references online package "facepunch.dm98"

To prove:

  1. Local uncompiled projects can reference remote packages
  2. Local projects can compile using remote packages
  3. Local projects can use remote package's base.dll instead of the local

Overview (`engine/Tests/Sandbox.Test.Unit/Layout/Tools/README.md`)

Layout conformance fixture tools

Both fixture sets come from upstream repositories and are regenerated with these scripts (Python 3):

  • convert_flex_fixtures.py /tests/generated ../FlexConformance - converts Yoga's Chrome-generated gtest fixtures
    (facebook/yoga, tag v3.2.1, MIT) into the [TestClass] files in ../FlexConformance/.
  • pack_layout_fixtures.py /tests/xml ../../Data/Layout block grid blockgrid gridflex - packs Taffy's
    Chrome-generated XML fixtures (DioxusLabs/taffy revision
    ac2b86929d35b7e0f1d24919595b89b4ce89baa4, MIT) into one layout-<group>.xml per group for
    BlockGridFixtureTests. The checked-in files match that revision byte-for-byte apart from their wrapper.
  • update_layout_signatures.py <results.trx> ../../Data/Layout/layout-known-failures.txt - prints an updated
    known-failure baseline after a deliberate layout change. Run the block/grid conformance tests with a TRX logger, review every
    changed mismatch, and use the output to update the baseline; never refresh signatures blindly.

Local suite and file names are neutral; generated source comments and XML headers retain upstream attribution.
Flex fixture factories explicitly assign omitted source styles; production nodes always use CSS defaults.
FixtureGeometry compares raw results in the source fixture's rounded coordinate convention without mutating
nodes. This is test observation only, not an alternate production layout mode. Block/grid fixtures retain
rectangle and child-count assertions; resolved-track diagnostics are no longer collected in production.
Full MIT notices are comments in Sandbox.Layout/Layout/Flex/FlexLayout.cs and Sandbox.Layout/Layout/Grid/TrackSizing.cs;
the layout assembly embeds these sources so binary distributions also retain the notices.
Signatures hash only the comparison lines (LF-normalized, with a trailing blank line), not diagnostic paths,
suite names, or input/actual tree dumps. Renaming the suites and data files does not require refreshing them.


Engine Tools InteropGen Docs Classes (`engine/Tools/InteropGen/docs/classes.md`)

Classes, structs and members

Declaring classes

text
native class IPhysicsBody as NativeEngine.IPhysicsBody
{
	void SetMass( float f );
	float GetMass();
}

Every class declaration starts with native or managed - which side implements it:

  • native class - implemented in C++, imported into managed. The generator emits a C# wrapper
    struct holding the native pointer, with a method per function.
  • managed class - implemented in C#, exported to native. The generator emits a C++ class
    (in the native header) whose methods call into managed, and [UnmanagedCallersOnly] thunks on
    the managed side. Instances are addressed by an object id resolved via Sandbox.InteropSystem.

The forms:

Form Meaning
native class X Instance class - wrapper carries a self pointer.
native static class X No instances; members are called as X::Member(). A static class whose native name starts with global binds free functions at global scope (::Member()).
native accessor g_pThing Like a static class, but members are reached through a global pointer: g_pThing->Member(). The generated thunk asserts the pointer isn't null.
managed class X / managed static class X Same split, implemented in C#.

Names and aliasing

as gives the type a different name on the other side:

text
native class CVideoPlayer as NativeEngine.CVideoPlayer
managed static class Sandbox.Engine.Bootstrap

The first name is the implementing side's name, the as name is the other side's. Namespaces use
. (or :: for native) and are split off automatically. Without as, both sides use the same
name.

Base classes

text
native class CSceneObject as Sandbox.SceneObject
native class CSceneModel as Sandbox.SceneModel : Sandbox.SceneObject

: Base references another class in the same def. The derived wrapper inherits all of the base's
non-static functions and gets implicit/explicit conversion operators to and from every base. The
conversions go through native dynamic_cast, because with multiple inheritance base pointers can
differ from derived pointers.

Functions

text
bool Play( string pUrl, string pExt );
static CVideoPlayer Create( Sandbox.VideoPlayer managedObject ); [new]
void Destroy(); [delete]
int GetMemRequired( int width, int height ) const;
  • static members don't take a self/instance argument.
  • A trailing const is accepted (and required to match a const native method on managed classes).
  • Overloads are fine - mangled export names are de-duplicated automatically.
  • A function named GetType is emitted as GetType_Native in C# (to avoid object.GetType).

Specials after the semicolon:

Special Meaning
[new] The thunk runs return new NativeType( args ); instead of calling a method.
[delete] The thunk runs delete (NativeType*)self;. On the managed side the wrapper also nulls its own pointer afterwards.

Inline functions

A function starting with inline carries its native body in the def file, instead of binding to an
existing native method:

text
inline void SetBoneName( string boneName )
{
	self->m_boneName = boneName;
	self->m_pBoneName = self->m_boneName.String();
}

The body is emitted into the generated .cpp. Parameters arrive already converted to their native
types under their declared names (self is the typed instance pointer; this-> is rewritten to
self->). return expressions are marshalled like a normal return value.

Variables

A field declaration generates a get/set thunk pair and a C# property:

text
native class SheetSequence_t
{
	uint m_nId;
	bool m_bClamp;
	float m_flTotalTime;
}

Structs, enums and pointer handles

Value types that cross the boundary are declared at file level:

text
native struct MeshTraceInput
native enum ButtonCode_t is NativeEngine.ButtonCode;
native pointer SwapChainHandle_t;
  • struct - both sides must define the type with an identical layout. Sizes are verified at
    startup (structSizes exchange); a mismatch is a fatal error. Structs 8 bytes or larger are
    passed by pointer when used as parameters; smaller structs must be marked [small] and are
    passed by value.
  • enum - passed as a 64-bit integer and cast on both sides.
  • pointer - a native type that pretends not to be a pointer (DECLARE_POINTER_HANDLE).
    Managed treats it as an opaque IntPtr-sized struct.

as / is (synonyms) alias the managed name, same as for classes.

Attributes

Attributes are written on the line(s) before a declaration and apply to the next class, struct or
function parsed.

On functions

Attribute Meaning
[nogc] Emits [SuppressGCTransition] - the call skips the GC transition, so it must be short, non-blocking and never call back into managed. On a class, applies to all of its functions.
[callback] Marks a function that can re-enter managed. Cancels [nogc] inherited from the class.

On classes

Attribute Meaning
[Handle:Managed.Type] Instances cross the boundary as handle ids instead of raw pointers. Managed resolves them with Sandbox.HandleIndex.Get<T>, native with GetManagedHandle.
[ResourceHandle:HModel] The class is accessed through a strong resource handle (HModelStrong). Thunks dereference the handle (returning default if unloaded), and the class automatically gains handle-management functions: DestroyStrongHandle, IsStrongHandleValid, IsError, IsStrongHandleLoaded, CopyStrongHandle, GetBindingPtr.
[SharedDataPointer] For Qt-style classes that are just a smart pointer to shared data. The wrapper is a public class (not a struct) that copies by value across the boundary and queues a Dispose from its finalizer.
[WindowsOnly] When generating on a non-Windows platform, the native thunks are emitted as stubs returning default values.

On structs

Attribute Meaning
[small] The struct is smaller than a pointer (8 bytes) and is passed by value. Required - the generated code static_asserts that unmarked structs are at least 8 bytes.

Engine Tools InteropGen Docs Def Files (`engine/Tools/InteropGen/docs/def-files.md`)

.def file format

A .def file is parsed line by line. Blank lines are skipped, and lines starting with // are
comments. Every other line is one of:

  • a directive - the first word is the keyword, the rest of the line is its argument
  • a type declaration - native class ..., managed struct ..., etc. (see classes.md)
  • an attribute - [nogc], [Handle:...], etc., applying to the next declaration

Directive arguments may be wrapped in double quotes; the quotes are stripped. The keyword doesn't
have to start the line - #include "foo.h" works because the parser matches the first word-run it
finds, which is handy for keeping C++ syntax highlighting happy in def files.

Top-level directives

Directive Example Meaning
ident ident "engine" Name of this binding set. Becomes the native init export igen_engine and namespaces handle types.
nativedll nativedll engine2.dll The native dll managed loads at startup (extension is stripped; resolved per-platform).
cs cs "../Sandbox.Engine/Interop.Engine.cs" Where to write the managed output, relative to the def file.
cpp cpp "../../src/engine2/interop.engine.cpp" Where to write the native source.
hpp hpp "../../src/engine2/interop.engine.h" Where to write the native header. Per-class sub-headers (interop.engine.<class>.h) are written next to it.
namespace namespace "Managed.SandboxEngine" The namespace for the generated NativeInterop/Exports classes - C# namespace on the managed side, C++ namespace on the native side.
exceptions exceptions "Sandbox.Interop.BindingException" Managed method called when an exported managed function throws: (string className, string functionName, Exception e).
pch pch "cbase.h" Precompiled header #included at the top of the generated .cpp.
include include "engine/*" Includes a header, def file or folder - see below.
inherit inherit "engine.def" This binding set builds on another - see below.
skipall skipall "tools.def" Skip everything another def already covers - see below.
delegate delegate DebugDrawDelegate_t; Declares a native delegate type name so it can be used as a parameter type (passed as IntPtr, converted with FunctionPointerToDelegate<T> on the native side).

include

include does three different things depending on its argument:

  • ends in .h โ†’ emitted as #include "..." in the generated native header. The conventional
    spelling is #include "vphysics2/iphysicsbody.h", which parses identically.
  • ends in .def โ†’ that file is parsed inline, as if its contents were pasted here. Paths are
    relative to the including file.
  • otherwise โ†’ treated as a folder: every *.def in it is included in alphabetical order.
    A trailing /* (e.g. include "common/*") recurses into subfolders.

Included def text counts toward the definition hash, so a change in any included file correctly
invalidates the binary pairing.

inherit and skipall

Several defs can cover overlapping native code. tools.def builds on engine.def:

text
ident "tools"
inherit "engine.def"
include "common/*"      // same folders engine.def includes

inherit loads the other def and remembers what it declared. Types declared there are still
parsed here (the includes overlap on purpose) so they can be referenced as parameter and return
types - but they are not re-emitted: their wrappers already exist in the inherited def's
assembly, and they're excluded from this def's export tables and struct size checks.

skipall is the stronger form used when a def chain gets deep (e.g. tools.hammer.def inherits
engine.def, tools.def and tools.assetsystem.def):

text
inherit "tools.def"
skipall "tools.def"

Everything that appears in the named def - classes (native and managed), structs, and its .h
includes - is skipped during emission here.

manifest.def

engine/manifest.def is just the list of defs to build, one per line:

text
Definitions/tools.def
Definitions/engine.def
...

Lines not ending in .def are ignored.


Engine Tools InteropGen Docs Types (`engine/Tools/InteropGen/docs/types.md`)

Argument types and flags

Every parameter, return value and variable in a def has a type. A type is either one of the
built-ins below, or a name resolved against the def's declared classes, structs, enums, pointers
and delegates.

Built-in types

Built-ins are Arg subclasses in Arguments/, registered by [TypeName] attribute (matching is
case-insensitive).

.def type C# C++ Notes
void void void Return type only.
bool bool bool Crosses the boundary as int (0/1).
byte byte unsigned char
ushort ushort unsigned short
int int int
uint uint unsigned int
long long int64
ulong ulong uint64
float float float
double double double
intptr, void* IntPtr void* Raw pointer.
string string const char* UTF-8. Pinned for the call going in; copied to a thread-local on the way out (SafeReturnString).
CUtlString string CUtlString Like string but the native side holds a CUtlString.
stringtoken Sandbox.StringToken uint32 Built native-side with StringTokenFromHashCode.

Qt types, used by the tools defs:

.def type C# C++
qstring string QString (crosses as const QChar*)
qdir string QDir
qbytearray string QByteArray (crosses base64-encoded)
qicon string resolved with FindOrCreateQIcon
qreal float qreal
qpoint, qpointf Vector3 QPoint / QPointF
qsize, qsizef Vector3 QSize / QSizeF
qcolor Color32 QColor
qrect, qrectf QRectF QRectF

Resolving everything else

Any other type name is looked up, in order:

  1. a declared class by managed name with namespace, then by short managed name
  2. a declared struct/enum/pointer by managed name (with or without namespace), then native name
  3. a declared delegate

An unknown type fails the build with Unknown Type <name>.

How a resolved type crosses the boundary depends on what it is:

  • native class - the raw native pointer (IntPtr in C#). [Handle:...] classes cross as a
    handle id, [ResourceHandle:...] classes as a strong-handle pointer.
  • managed class - a uint object id, resolved through Sandbox.InteropSystem.
  • struct - by pointer if 8+ bytes, by value if marked [small].
  • enum - as a 64-bit integer, cast on both sides.
  • pointer - as an opaque IntPtr.
  • delegate - as an IntPtr, converted native-side with FunctionPointerToDelegate<T>.

Parameter flags

Flags are written before the type:

text
void GetEngineSwapChainSize( out int w, out int h );
void SetTonemapParameters( ref SceneTonemapParameters_t pParams );
bool ConvertImageFormat( CastTo[uint8*] void* src, ... );
Flag Sides Meaning
out C# out param Passed to native as a pointer to write through. out string works too - the native side writes a pointer, managed converts it after the call.
ref C# ref param Passed as a pointer, readable and writable.
asref return values The wrapper returns ref T - a managed ref into native memory (Unsafe.AsRef).
cref native side The native method wants a reference (T&): the thunk dereferences the incoming pointer, and outgoing calls pass &value.
cast native side Cast the value to the parameter's native type when passing it.
CastTo[X] native side Cast the incoming value to the exact native type X before use - for types managed doesn't model (e.g. CastTo[CKV3MemberName] string key).
stable string returns Promises the returned const char* outlives the call (an interned name, a member that isn't going away), so the thunk skips the defensive copy into a thread-local that string returns normally get. Only valid on string - don't use it for pointers into temporaries.

Arrays and literals

  • type[] name - passes a raw pointer to the first element (type* on both sides). The caller
    guarantees the length by convention (usually a separate count parameter).
  • [expression] as a parameter is a literal: it isn't part of the managed signature at all, and the
    native call site passes the expression verbatim.

--- METRICS ---