Skip to content

Commit

Permalink
Fix multi-threaded renderer in Godot 4.3
Browse files Browse the repository at this point in the history
  • Loading branch information
pkdawson committed Sep 9, 2024
1 parent 846a012 commit 1b58907
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 35 deletions.
14 changes: 7 additions & 7 deletions addons/imgui-godot/ImGuiGodot/Internal/RdRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ protected void RenderOne(Rid fb, ImDrawDataPtr drawData)
// allocate merged index and vertex buffers
if (_idxBufferSize < drawData.TotalIdxCount)
{
if (_idxBuffer.Id != 0)
if (_idxBuffer.IsValid)
RD.FreeRid(_idxBuffer);
_idxBuffer = RD.IndexBufferCreate(
(uint)drawData.TotalIdxCount,
Expand All @@ -304,17 +304,17 @@ protected void RenderOne(Rid fb, ImDrawDataPtr drawData)

if (_vtxBufferSize < drawData.TotalVtxCount)
{
if (_vtxBuffer.Id != 0)
if (_vtxBuffer.IsValid)
RD.FreeRid(_vtxBuffer);
_vtxBuffer = RD.VertexBufferCreate((uint)(drawData.TotalVtxCount * vertSize));
_vtxBufferSize = drawData.TotalVtxCount;
}

// check if our font texture is still valid
foreach (var kv in _uniformSets)
foreach (var (texid, uniformSetRid) in _uniformSets)
{
if (!RD.UniformSetIsValid(kv.Value))
_uniformSets.Remove(kv.Key);
if (!RD.UniformSetIsValid(uniformSetRid))
_uniformSets.Remove(texid);
}

if (drawData.CmdListsCount > 0)
Expand Down Expand Up @@ -396,9 +396,9 @@ public void Dispose()
{
RD.FreeRid(_sampler);
RD.FreeRid(_shader);
if (_idxBuffer.Id != 0)
if (_idxBuffer.IsValid)
RD.FreeRid(_idxBuffer);
if (_vtxBuffer.Id != 0)
if (_vtxBuffer.IsValid)
RD.FreeRid(_vtxBuffer);
}

Expand Down
64 changes: 46 additions & 18 deletions addons/imgui-godot/ImGuiGodot/Internal/RdRendererThreadSafe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ public DisposableList(int capacity) : base(capacity) { }

public void Dispose()
{
foreach (var tuple in this)
foreach (var (_, u) in this)
{
tuple.Item2.Dispose();
u.Dispose();
}
Clear();
}
Expand All @@ -69,7 +69,41 @@ internal sealed class RdRendererThreadSafe : RdRenderer, IRenderer
{
public new string Name => "godot4_net_rd_mt";

private readonly object _sharedDataLock = new();
#if GODOT4_3_OR_GREATER
public new void Render()
{
var pio = ImGui.GetPlatformIO();
var newData = new SharedList(pio.Viewports.Size);

for (int i = 0; i < pio.Viewports.Size; ++i)
{
var vp = pio.Viewports[i];
if (vp.Flags.HasFlag(ImGuiViewportFlags.IsMinimized))
continue;

Rid vprid = Util.ConstructRid((ulong)vp.RendererUserData);
newData.Add(new(vprid, new(vp.DrawData)));
}

RenderingServer.CallOnRenderThread(Callable.From(() => DrawOnRenderThread(newData)));
}

private void DrawOnRenderThread(SharedList dataArray)
{
foreach (var (vprid, clone) in dataArray)
{
Rid fb = GetFramebuffer(vprid);
if (RD.FramebufferIsValid(fb))
{
ReplaceTextureRids(clone.Data);
RenderOne(fb, clone.Data);
}
}

FreeUnusedTextures();
dataArray.Dispose();
}
#else
private SharedList? _dataToDraw;

public RdRendererThreadSafe()
Expand Down Expand Up @@ -99,36 +133,30 @@ public RdRendererThreadSafe()
newData.Add(new(GetFramebuffer(vprid), new(vp.DrawData)));
}

lock (_sharedDataLock)
{
// if a frame was skipped, free old data
_dataToDraw?.Dispose();
_dataToDraw = newData;
}
// if a frame was skipped, free old data
var oldData = Interlocked.Exchange(ref _dataToDraw, newData);
oldData?.Dispose();
}

private SharedList TakeSharedData()
{
lock (_sharedDataLock)
{
var rv = _dataToDraw;
_dataToDraw = null;
return rv ?? [];
}
var rv = Interlocked.Exchange(ref _dataToDraw, null);
return rv ?? [];
}

private void OnFramePreDraw()
{
// take ownership of shared data
using SharedList dataArray = TakeSharedData();

foreach (var kv in dataArray)
foreach (var (fb, clone) in dataArray)
{
if (RD.FramebufferIsValid(kv.Item1))
RenderOne(kv.Item1, kv.Item2.Data);
if (RD.FramebufferIsValid(fb))
RenderOne(fb, clone.Data);
}

FreeUnusedTextures();
}
#endif
}
#endif
6 changes: 4 additions & 2 deletions addons/imgui-godot/ImGuiGodot/Widgets.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#if GODOT_PC
using Godot;
using ImGuiNET;
using System;
Expand Down Expand Up @@ -135,7 +134,7 @@ public static bool ImageButton(
Vector4 bg_col,
Vector4 tint_col)
{
(Vector2 uv0, Vector2 uv1) = GetAtlasUVs(tex);
var (uv0, uv1) = GetAtlasUVs(tex);
return ImGui.ImageButton(str_id, (IntPtr)tex.GetRid().Id, size, uv0, uv1, bg_col, tint_col);
}

Expand All @@ -149,4 +148,7 @@ private static (Vector2 uv0, Vector2 uv1) GetAtlasUVs(AtlasTexture tex)
#pragma warning restore IDE0004 // Remove Unnecessary Cast
}
}

#if NET9_0_OR_GREATER
// TODO: implicit extension GodotWidgets for ImGui
#endif
57 changes: 49 additions & 8 deletions gdext/src/RdRendererThreadSafe.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "RdRendererThreadSafe.h"
#include "common.h"
#include <godot_cpp/classes/display_server.hpp>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/rendering_server.hpp>
#include <mutex>
Expand Down Expand Up @@ -44,17 +45,33 @@ struct RdRendererThreadSafe::Impl
ImDrawData* data;
};

bool isGodot42 = false;

using SharedData = std::pair<RID, std::unique_ptr<ClonedDrawData>>;

// mutex overhead should be minimal (near-zero contention), and refactoring to atomic is ugly
std::mutex sharedDataMutex;
std::vector<SharedData> dataToDraw;
};

RdRendererThreadSafe::RdRendererThreadSafe() : impl(std::make_unique<Impl>())
{
RenderingServer* RS = RenderingServer::get_singleton();
RS->connect("frame_pre_draw",
Callable(Engine::get_singleton()->get_singleton("ImGuiController"), "on_frame_pre_draw"));
// TODO: remove 4.2 compatibility when 4.4 is released
Dictionary vinfo = Engine::get_singleton()->get_version_info();
impl->isGodot42 = (int)vinfo["hex"] < 0x040300;

if (impl->isGodot42)
{
RenderingServer* RS = RenderingServer::get_singleton();
RS->connect("frame_pre_draw",
Callable(Engine::get_singleton()->get_singleton("ImGuiController"), "on_frame_pre_draw"));
}

if (DisplayServer::get_singleton()->window_get_vsync_mode() == DisplayServer::VSYNC_DISABLED)
{
UtilityFunctions::push_warning(
"[imgui-godot] Multi-threaded renderer with vsync disabled will probably crash");
}
}

RdRendererThreadSafe::~RdRendererThreadSafe()
Expand All @@ -72,16 +89,29 @@ void RdRendererThreadSafe::Render()
if (vp->Flags & ImGuiViewportFlags_IsMinimized)
continue;

ReplaceTextureRIDs(vp->DrawData);
RID vprid = make_rid(vp->RendererUserData);
newData[i].first = GetFramebuffer(vprid);
if (impl->isGodot42)
{
ReplaceTextureRIDs(vp->DrawData);
newData[i].first = GetFramebuffer(vprid);
}
else
{
newData[i].first = vprid;
}
newData[i].second = std::make_unique<Impl::ClonedDrawData>(vp->DrawData);
}

{
std::unique_lock<std::mutex> lock(impl->sharedDataMutex);
impl->dataToDraw = std::move(newData);
}

if (!impl->isGodot42)
{
RenderingServer::get_singleton()->call_on_render_thread(
Callable(Engine::get_singleton()->get_singleton("ImGuiController"), "on_frame_pre_draw"));
}
}

void RdRendererThreadSafe::OnFramePreDraw()
Expand All @@ -95,10 +125,21 @@ void RdRendererThreadSafe::OnFramePreDraw()
}

RenderingDevice* RD = RenderingServer::get_singleton()->get_rendering_device();
for (auto& kv : dataArray)
for (const auto& kv : dataArray)
{
if (RD->framebuffer_is_valid(kv.first))
RdRenderer::Render(kv.first, kv.second->data);
RID fb = kv.first;
ImDrawData* drawData = kv.second->data;

if (!impl->isGodot42)
{
fb = GetFramebuffer(fb);
ReplaceTextureRIDs(drawData);
}

if (RD->framebuffer_is_valid(fb))
{
RdRenderer::Render(fb, drawData);
}
}

FreeUnusedTextures();
Expand Down

0 comments on commit 1b58907

Please sign in to comment.