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
18 changes: 16 additions & 2 deletions docs/renderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,22 @@ flushed from the render thread to the transfer queue
- **SDL/swapchain path**: uses acquire/present semaphores and transitions to
`ePresentSrcKHR` in `recordFinalize`
- **Shared texture path (editor)**: no acquire/present semaphore dependency, transitions
color attachment to transfer source, copies to host-visible staging, and publishes the
latest completed frame for `toast_viewport_get_frame`
color attachment to transfer source and publishes only completed frames. Windows shares
BGRA textures with a keyed mutex; Linux blits into RGBA textures for Avalonia's OpenGL
importer and releases them in transfer-source layout, using ready/done semaphores.
On X11 the editor uses Avalonia's default GLX compositor. Linux shared images
use non-dedicated Vulkan allocations because Avalonia's OpenGL importer does
not mark imported memory objects as dedicated; mismatching those flags can
produce transparent-black frames.
CPU readback remains BGRA and runs only on request while sharing is active.
The editor reserves the exact generation, slot, and frame ID before importing a frame
into its drawing surface. In-flight, reserved, and displayed slots cannot be overwritten;
acquisition returns `eNotReady` when no slot is free. Dropped frames' ready signals are
consumed before a slot signals readiness again. Presentation starts from animation-frame
callbacks and can pick up a newer frame after an update completes via a low-priority
dispatcher job; this avoids waiting an extra animation tick without starving input.
Set `TOAST_VIEWPORT_READBACK=1` when starting the editor to bypass GPU sharing
and compare the CPU readback path while diagnosing display artifacts.

### Resizing

Expand Down
10 changes: 10 additions & 0 deletions editor/Assets/VoxelPaletteFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public sealed class VoxelPaletteEntry {

public float Alpha { get; set; } = 1f;

/// <summary>Mirrors voxel::k_default_ior</summary>
public const float DefaultIor = 1.5f;
public const float MaxIor = 3.55f;

public float Ior { get; set; } = DefaultIor;

public int Material { get; set; }

/// <summary>Authored without a material which differs from material 0</summary>
Expand All @@ -40,6 +46,7 @@ public void Clear() {
R = G = B = 0;
Roughness = Metallic = Reflectivity = Emissive = 0f;
Alpha = 1f;
Ior = DefaultIor;
TransformsTo = 0;
Transparent = false;
}
Expand Down Expand Up @@ -132,6 +139,7 @@ public static VoxelPaletteFile FromString(string toml) {
target.Reflectivity = GetFloat(entry, "reflectivity", 0f);
target.Emissive = GetFloat(entry, "emissive", 0f);
target.Alpha = GetFloat(entry, "alpha", 1f);
target.Ior = GetFloat(entry, "ior", VoxelPaletteEntry.DefaultIor);

target.HasMaterial = entry.ContainsKey("material");
target.Material = GetInt(entry, "material", 0);
Expand Down Expand Up @@ -185,6 +193,8 @@ public string Serialize() {
["emissive"] = (double)Clamp01(entry.Emissive),
["alpha"] = (double)Clamp01(entry.Alpha)
};
if (Math.Abs(entry.Ior - VoxelPaletteEntry.DefaultIor) > 0.005f)
table["ior"] = Math.Round((double)Math.Clamp(entry.Ior, 1f, VoxelPaletteEntry.MaxIor), 2);
if (entry.HasMaterial) table["material"] = (long)Math.Clamp(entry.Material, 0, SlotCount - 1);
table["transforms_to"] = (long)Math.Clamp(entry.TransformsTo, 0, 255);
table["transparent"] = entry.Transparent;
Expand Down
12 changes: 11 additions & 1 deletion editor/Editors/PaletteView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@
</Border>

<Grid ColumnDefinitions="Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
RowSpacing="6"
ColumnSpacing="8"
Margin="0, -4, 0, 0">
Expand Down Expand Up @@ -332,6 +332,16 @@
Value="{Binding Alpha, Mode=TwoWay}"
Minimum="0"
Maximum="1" />

<TextBlock Grid.Row="8"
Grid.Column="0"
Classes="lbl"
Text="IOR" />
<elements:DragFloatBox Grid.Row="8"
Grid.Column="1"
Value="{Binding Ior, Mode=TwoWay}"
Minimum="1"
Maximum="3.55" />
</Grid>

<Grid ColumnDefinitions="Auto,*"
Expand Down
8 changes: 8 additions & 0 deletions editor/Editors/PaletteViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public partial class PaletteViewModel : Tool, IToastZoneEditor, IAutosavable {
[ObservableProperty] private float m_roughness;
[ObservableProperty] private int m_selectedIndex = 1;
[ObservableProperty] private float m_alpha = 1f;
[ObservableProperty] private float m_ior = VoxelPaletteEntry.DefaultIor;
[ObservableProperty] private PaletteSlotVM? m_selectedSlot;
[ObservableProperty] private int m_transformsTo;
[ObservableProperty] private bool m_transparent;
Expand Down Expand Up @@ -421,6 +422,7 @@ private void LoadSelectedEntry() {
Reflectivity = entry.Reflectivity;
Emissive = entry.Emissive;
Alpha = entry.Alpha;
Ior = entry.Ior;
TransformsTo = entry.TransformsTo;
Transparent = entry.Transparent;
LoadSelectedMaterial();
Expand Down Expand Up @@ -590,6 +592,12 @@ partial void OnAlphaChanged(float value) {
TouchEntry();
}

partial void OnIorChanged(float value) {
if (Ignore || Selected is not { } entry) return;
entry.Ior = Math.Clamp(value, 1f, VoxelPaletteEntry.MaxIor);
TouchEntry();
}

partial void OnIsDirtyChanged(bool value) {
UpdateTitle();
}
Expand Down
12 changes: 9 additions & 3 deletions editor/Engine/ToastEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ public static void SetShowDebugWindows(bool visible) {
toast_set_show_debug_windows(visible ? 1 : 0);
}

/// Overlay up in game view so the cursor stays free even under a game lock
public static bool DebugOverlayOwnsCursor => IsEngineReady && toast_debug_overlay_owns_cursor() != 0;

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

Expand Down Expand Up @@ -294,8 +297,8 @@ public bool ExportSharedViewportSlot(ulong generation, uint slot, out ToastViewp
return IsEngineReady && toast_viewport_export_shared_slot(generation, slot, out handles) == 1;
}

public bool HoldSharedViewportSlot(ulong generation, uint slot) =>
IsEngineReady && toast_viewport_hold_shared_slot(generation, slot) == 1;
public bool HoldSharedViewportSlot(ulong generation, uint slot, ulong frameId) =>
IsEngineReady && toast_viewport_hold_shared_slot(generation, slot, frameId) == 1;

public void ReleaseSharedViewportSlot(ulong generation, uint slot, bool consumed) {
if (IsEngineReady) toast_viewport_release_shared_slot(generation, slot, consumed ? 1 : 0);
Expand Down Expand Up @@ -553,7 +556,7 @@ private static partial int toast_viewport_export_shared_slot(
ulong generation, uint slot, out ToastViewportSharedHandles outHandles);

[LibraryImport(EngineLib)]
private static partial int toast_viewport_hold_shared_slot(ulong generation, uint slot);
private static partial int toast_viewport_hold_shared_slot(ulong generation, uint slot, ulong frameId);

[LibraryImport(EngineLib)]
private static partial void toast_viewport_release_shared_slot(ulong generation, uint slot, int consumed);
Expand Down Expand Up @@ -600,6 +603,9 @@ public static void ReloadManifest() {
[LibraryImport(EngineLib)]
private static partial void toast_set_show_debug_windows(int visible);

[LibraryImport(EngineLib)]
private static partial int toast_debug_overlay_owns_cursor();

public static void ReloadProjectSettings() {
if (IsEngineReady) OnTickThread(toast_reload_project_settings);
}
Expand Down
23 changes: 16 additions & 7 deletions editor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,26 @@ public static AppBuilder BuildAvaloniaApp() {
#endif
.WithInterFont();

// The DXGI swap chain ticks on the vblank of the fastest monitor where WinUI commits can lag the refresh
var win32 = new Win32PlatformOptions {
CompositionMode = [
Win32CompositionMode.LowLatencyDxgiSwapChain,
Win32CompositionMode.WinUIComposition,
Win32CompositionMode.DirectComposition,
Win32CompositionMode.RedirectionSurface
]
};

// Force software when renderdoc
if (GraphicsDebuggerDetector.IsAttached) {
builder = builder
.With(new Win32PlatformOptions {
RenderingMode = [ Win32RenderingMode.Software ]
})
.With(new X11PlatformOptions {
RenderingMode = [ X11RenderingMode.Software ]
});
win32.RenderingMode = [ Win32RenderingMode.Software ];
builder = builder.With(new X11PlatformOptions {
RenderingMode = [ X11RenderingMode.Software ]
});
}

builder = builder.With(win32);

return builder.LogToTrace();
}
}
9 changes: 9 additions & 0 deletions editor/VoxelEditor/VoxelPalettePicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ public float Alpha {
set => Set(() => m_entry.Alpha = value);
}

public float Ior {
get => m_entry.Ior;
set => Set(() => m_entry.Ior = Math.Clamp(value, 1f, VoxelPaletteEntry.MaxIor));
}

private static byte ToByte(float value) {
return (byte)Math.Clamp((int)Math.Round(value * 255f), 0, 255);
}
Expand Down Expand Up @@ -326,6 +331,10 @@ private Control BuildPopup() {
transparent.Bind(BoolBox.ValueProperty, new Binding(nameof(PaletteEntryVM.Transparent)) { Mode = BindingMode.TwoWay });
properties.Children.Add(Row("Transparent", transparent));
properties.Children.Add(Row("Alpha", Number(nameof(PaletteEntryVM.Alpha))));
var ior = Number(nameof(PaletteEntryVM.Ior), false);
ior.Minimum = 1;
ior.Maximum = VoxelPaletteEntry.MaxIor;
properties.Children.Add(Row("IOR", ior));
}

var content = new StackPanel {
Expand Down
27 changes: 22 additions & 5 deletions editor/Workspace/SharedViewportPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using editor.Engine;

namespace editor.Workspace;

internal sealed class SharedViewportPresenter : IDisposable {
private readonly Visual m_host;
private readonly ToastEngine m_engine;
private readonly Action m_onPresented;
private readonly Func<bool> m_canPresent;

private Compositor? m_compositor;
private ICompositionGpuInterop? m_interop;
Expand All @@ -33,9 +36,11 @@ private sealed class Slot {
public ICompositionImportedGpuSemaphore? Done;
}

public SharedViewportPresenter(Visual host, ToastEngine engine) {
public SharedViewportPresenter(Visual host, ToastEngine engine, Action onPresented, Func<bool> canPresent) {
m_host = host;
m_engine = engine;
m_onPresented = onPresented;
m_canPresent = canPresent;
}

public bool Failed { get; private set; }
Expand Down Expand Up @@ -107,6 +112,11 @@ public bool Tick() {
}

Layout();
return TryPresentLatest();
}

private bool TryPresentLatest() {
if (!Active || m_surface is null || m_updating) return false;

var result = m_engine.TryGetSharedViewportFrame(out var frame);
if (result < 0) {
Expand All @@ -118,7 +128,7 @@ public bool Tick() {
if (frame.generation != m_generation && !Import(frame)) return false;

var slot = m_slots[frame.slot];
if (slot.Image is null || !m_engine.HoldSharedViewportSlot(m_generation, frame.slot)) return false;
if (slot.Image is null || !m_engine.HoldSharedViewportSlot(m_generation, frame.slot, frame.frame_id)) return false;

m_lastFrameId = frame.frame_id;
m_updating = true;
Expand All @@ -141,6 +151,11 @@ private async Task PresentAsync(ulong generation, uint index, Slot slot) {
}
m_engine.ReleaseSharedViewportSlot(generation, index, true);
m_updating = false;
// The compositor has finished the snapshot. Pick up a newer frame without
// waiting another animation tick, but let input run ahead of viewport work.
Dispatcher.UIThread.Post(() => {
if (Active && m_canPresent() && TryPresentLatest()) m_onPresented();
}, DispatcherPriority.Background);
}

private bool Import(ToastViewportSharedFrame frame) {
Expand All @@ -152,7 +167,8 @@ private bool Import(ToastViewportSharedFrame frame) {
var properties = new PlatformGraphicsExternalImageProperties {
Width = (int)frame.width,
Height = (int)frame.height,
Format = PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm,
Format = UsesKeyedMutex ? PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm
: PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm,
MemorySize = frame.memory_size,
TopLeftOrigin = true,
};
Expand Down Expand Up @@ -180,8 +196,9 @@ private void Layout() {
var scale = Math.Min(bounds.Width / m_width, bounds.Height / m_height);
if (!double.IsFinite(scale) || scale <= 0) return;
var size = new Vector2((float)(m_width * scale), (float)(m_height * scale));
m_visual.Size = size;
m_visual.Offset = new Vector3((float)((bounds.Width - size.X) / 2), (float)((bounds.Height - size.Y) / 2), 0);
var offset = new Vector3((float)((bounds.Width - size.X) / 2), (float)((bounds.Height - size.Y) / 2), 0);
if (m_visual.Size != size) m_visual.Size = size;
if (m_visual.Offset != offset) m_visual.Offset = offset;
}

private void DisposeSlots() {
Expand Down
Loading
Loading