Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/examples/async_compute/debris.slang
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Mirrors DebrisSimulation::Params
struct Params
{
float dt;
uint count;
};

[[vk::push_constant]]
Params gParams;

// xyz position w radius
[[vk::binding(0, 0)]]
StructuredBuffer<float4> gPrevious;

[[vk::binding(1, 0)]]
RWStructuredBuffer<float4> gCurrent;

// xyz velocity w unused
[[vk::binding(2, 0)]]
RWStructuredBuffer<float4> gVelocities;

static const float3 kGravity = float3(0.0, -9.81, 0.0);

[shader("compute")]
[numthreads(64, 1, 1)]
void integrateMain(uint3 id : SV_DispatchThreadID)
{
const uint i = id.x;
if (i >= gParams.count)
{
return;
}

float4 particle = gPrevious[i];
float3 velocity = gVelocities[i].xyz + kGravity * gParams.dt;
particle.xyz += velocity * gParams.dt;

// Ground plane bounce so the example stays on screen
if (particle.y < particle.w)
{
particle.y = particle.w;
velocity.y = -velocity.y * 0.4;
velocity.xz *= 0.8;
}

gCurrent[i] = particle;
gVelocities[i] = float4(velocity, 0.0);
}
42 changes: 42 additions & 0 deletions docs/examples/async_compute/debris_simulation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/// @file debris_simulation.cpp
/// @author dario
/// @date 10/10/2026

#include "debris_simulation.hpp"

#include <algorithm>

namespace example {

namespace {
constexpr vk::DeviceSize k_particle_bytes = sizeof(glm::vec4) * DebrisSimulation::k_max_particles;
constexpr uint32_t k_workgroup_size = 64;
}

DebrisSimulation::DebrisSimulation()
: AsyncComputeTask({.shader_uri = "core://shaders/debris.slang", .entries = {"integrateMain"}, .debug_name = "Debris"}) {
// The renderer draws positions and gameplay reads them back. Velocities never leave the task
m_positions = addRingBuffer(k_particle_bytes, true);
m_velocities = addPersistentBuffer(k_particle_bytes);

// Mirrors the bindings in debris.slang
bind(0, m_positions, Copy::previous);
bind(1, m_positions, Copy::current);
bind(2, m_velocities);
}

void DebrisSimulation::spawn(std::span<const glm::vec4> positions, std::span<const glm::vec4> velocities) {
m_count = static_cast<uint32_t>(std::min({positions.size(), velocities.size(), size_t {k_max_particles}}));
upload(m_positions, positions.first(m_count));
upload(m_velocities, velocities.first(m_count));
}

void DebrisSimulation::step(float dt) {
if (m_count == 0) {
return;
}
const Params params {.dt = dt, .count = m_count};
AsyncComputeTask::step([&](StepContext& ctx) { ctx.dispatch((m_count + k_workgroup_size - 1) / k_workgroup_size, params); });
}

}
49 changes: 49 additions & 0 deletions docs/examples/async_compute/debris_simulation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/// @file debris_simulation.hpp
/// @author dario
/// @date 10/10/2026

#pragma once

#include <glm/glm.hpp>
#include <span>
#include <toast/renderer/async_compute_task.hpp>

namespace example {

/// Debris particles integrated on the async compute queue
class DebrisSimulation : public renderer::AsyncComputeTask {
public:
static constexpr uint32_t k_max_particles = 65536;

DebrisSimulation();

/// Physics thread before the first step
void spawn(std::span<const glm::vec4> positions, std::span<const glm::vec4> velocities);

/// Physics thread. Returns right away
void step(float dt);

/// Physics thread. Newest finished positions and false until one exists
auto readPositions(std::span<glm::vec4> out) -> bool {
return read(m_positions, out);
}

/// For the render pass. xyz position w radius
[[nodiscard]]
auto positionsId() const -> BufferId {
return m_positions;
}

private:
/// Mirrors debris.slang Params
struct Params {
float dt;
uint32_t count;
};

BufferId m_positions = 0;
BufferId m_velocities = 0;
uint32_t m_count = 0;
};

}
39 changes: 39 additions & 0 deletions editor/Engine/ToastEngine.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
Expand Down Expand Up @@ -127,6 +128,12 @@ public static void SetWindowState(bool focused, bool minimized) {
toast_set_window_state(focused ? 1 : 0, minimized ? 1 : 0);
}

/// The ImGui overlays drawn over the viewport, such as the performance window
public static void SetShowDebugWindows(bool visible) {
if (!IsEngineReady) return;
toast_set_show_debug_windows(visible ? 1 : 0);
}

public string ProjectPath { get; }
public string CorePath { get; }

Expand Down Expand Up @@ -307,6 +314,8 @@ private static string EngineDllPath() {
private void TickLoop(CancellationToken token) {
m_tickThreadId = Environment.CurrentManagedThreadId;
m_ticking = true;
var stopwatch = Stopwatch.StartNew();
var nextTick = stopwatch.Elapsed;
try {
while (!token.IsCancellationRequested && toast_should_close() != 1) {
// Wait until the gate is open
Expand All @@ -322,6 +331,8 @@ private void TickLoop(CancellationToken token) {
} finally {
m_tickIdle.Set();
}

PaceTick(stopwatch, ref nextTick, token);
}
} catch (OperationCanceledException) {
// Dispose() cancels the token while the loop is on m_tickGate
Expand All @@ -331,6 +342,28 @@ private void TickLoop(CancellationToken token) {
}
}

// Max tick rate comes from the project settings, which only change on the tick thread
private static void PaceTick(Stopwatch stopwatch, ref TimeSpan nextTick, CancellationToken token) {
var maxRate = toast_get_max_tick_rate();
if (maxRate == 0) {
nextTick = stopwatch.Elapsed;
return;
}

var interval = TimeSpan.FromSeconds(1.0 / maxRate);
nextTick += interval;
var remaining = nextTick - stopwatch.Elapsed;
if (remaining <= TimeSpan.Zero) {
// fell behind, resync instead of bursting to catch up
Comment thread
dariormt marked this conversation as resolved.
nextTick = stopwatch.Elapsed;
return;
}

if (remaining > TimeSpan.FromMilliseconds(2))
token.WaitHandle.WaitOne(remaining - TimeSpan.FromMilliseconds(1));
while (stopwatch.Elapsed < nextTick && !token.IsCancellationRequested) Thread.SpinWait(50);
}

private void CreateMainWindow() {
var desktop = (IClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
desktop.MainWindow = new MainWindowView(this) {
Expand Down Expand Up @@ -487,6 +520,12 @@ public static void ReloadManifest() {
[LibraryImport(EngineLib)]
private static partial void toast_reload_project_settings();

[LibraryImport(EngineLib)]
private static partial uint toast_get_max_tick_rate();

[LibraryImport(EngineLib)]
private static partial void toast_set_show_debug_windows(int visible);

public static void ReloadProjectSettings() {
if (IsEngineReady) OnTickThread(toast_reload_project_settings);
}
Expand Down
4 changes: 4 additions & 0 deletions editor/Workspace/MainWindowView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@
Header="_Restore"
Click="OnMenuRestore" />
<Separator />
<MenuItem Header="Show _Debug windows"
ToggleType="CheckBox"
IsChecked="{Binding ShowDebugWindows, Mode=TwoWay}" />
<Separator />
<!-- everything here is generated by code -->
</MenuItem>
<MenuItem Header="_Help">
Expand Down
2 changes: 1 addition & 1 deletion editor/Workspace/MainWindowView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
namespace editor.Workspace;

public partial class MainWindowView : Window {
private const int StaticWindowMenuItems = 4;
private const int StaticWindowMenuItems = 6;
private readonly ToastEngine? m_toast;
private readonly Border? m_toastBorder;

Expand Down
5 changes: 5 additions & 0 deletions editor/Workspace/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public partial class MainWindowViewModel : ViewModelBase, IDisposable {
[ObservableProperty] private bool m_historyVisible;
[ObservableProperty] private bool m_locksVisible;
[ObservableProperty] private bool m_inspectorVisible = true;
[ObservableProperty] private bool m_showDebugWindows = true;
[ObservableProperty] private bool m_signalsVisible = true;
[ObservableProperty] private bool m_logsVisible = true;
[ObservableProperty] private IRootDock m_mainLayout;
Expand Down Expand Up @@ -375,6 +376,10 @@ partial void OnCurveEditorVisibleChanged(bool value) {
ToggleToastTool("Curve", value);
}

partial void OnShowDebugWindowsChanged(bool value) {
ToastEngine.SetShowDebugWindows(value);
}

partial void OnTableEditorVisibleChanged(bool value) {
ToggleToastTool("Table", value);
}
Expand Down
4 changes: 4 additions & 0 deletions engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,12 @@ if (TOAST_UNITY_BUILD)
set_target_properties(toast_engine PROPERTIES UNITY_BUILD ON UNITY_BUILD_BATCH_SIZE 8)
endif ()

# Vulkan synchronization validation
option(TOAST_SYNC_VALIDATION "Enable Vulkan synchronization validation in Debug builds" ON)

target_compile_definitions(toast_engine PUBLIC
$<$<CONFIG:Debug>:DEBUG>
$<$<AND:$<CONFIG:Debug>,$<BOOL:${TOAST_SYNC_VALIDATION}>>:TOAST_SYNC_VALIDATION>
$<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>>:NDEBUG>
$<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:TRACY_ENABLE>
$<$<CONFIG:RelWithDebInfo>:TOAST_LUA_OWNERSHIP_CHECKS>
Expand Down
7 changes: 7 additions & 0 deletions engine/assets/schemas/project_settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"description": "Each entry X creates an X:// VFS scheme rooted at {ProjectPath}/X",
"default": ["assets"]
},
"max_tick_rate": {
"type": "integer",
"x-toast-type": "int",
"minimum": 0,
"description": "Maximum game ticks per second in the editor and the player; 0 is uncapped",
"default": 500
},
"gameplay": {
"x-toast-type": "gameplay",
"description": "Gameplay entry-point settings"
Expand Down
54 changes: 54 additions & 0 deletions engine/assets/shaders/depth_convention.slang
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Mirrors renderer/depth_convention.hpp through TOAST_REVERSED_Z
// Shadow maps keep standard depth and do not use these

// The depth buffer clears to it
public static const float kFarDepth = TOAST_REVERSED_Z ? 0.0 : 1.0;

public static const float kNearDepth = TOAST_REVERSED_Z ? 1.0 : 0.0;

// Sky pixels
public bool isFarDepth(float depth)
{
#if TOAST_REVERSED_Z
return depth <= 0.0;
#else
return depth >= 1.0;
#endif
}

public bool isCloserDepth(float a, float b)
{
#if TOAST_REVERSED_Z
return a > b;
#else
return a < b;
#endif
}

public float closerDepth(float a, float b)
{
#if TOAST_REVERSED_Z
return max(a, b);
#else
return min(a, b);
#endif
}

public float fartherDepth(float a, float b)
{
#if TOAST_REVERSED_Z
return min(a, b);
#else
return max(a, b);
#endif
}

// Clip space distance from the near plane and >= 0 on or beyond it
public float clipNearDistance(float4 clip)
{
#if TOAST_REVERSED_Z
return clip.w - clip.z;
#else
return clip.z;
#endif
}
4 changes: 4 additions & 0 deletions engine/assets/shaders/depth_convention.slang.meta
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
uid = "olYeZGySqTs"
type = "slang_module"
created_at = "2026-10-09T17:15:00.0000000Z"
modified_at = "2026-10-09T17:15:00.0000000Z"
6 changes: 4 additions & 2 deletions engine/assets/shaders/environment.slang
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import depth_convention;

struct EnvironmentParams
{
float4 faceRight;
Expand Down Expand Up @@ -80,13 +82,13 @@ float4 fragmentEquirect(VSOutput input) : SV_Target0
return float4(gEquirectangular.SampleLevel(uv, 0.0).rgb * gParams.intensity, 1.0);
}

// Far plane so it needs a less or equal depth test
// Far plane so it needs a closer or equal depth test
[shader("vertex")]
VSOutput vertexSkybox(uint vertexId : SV_VertexID)
{
VSOutput output;
output.uv = float2((vertexId << 1) & 2, vertexId & 2);
output.position = float4(output.uv * 2.0 - 1.0, 1.0, 1.0);
output.position = float4(output.uv * 2.0 - 1.0, kFarDepth, 1.0);
return output;
}

Expand Down
Loading
Loading