Skip to content
Open
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
10 changes: 8 additions & 2 deletions src/BizHawk.Client.Common/Api/Classes/GuiApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,15 @@ public void DrawNew(string name, bool clear)
public void AddMessage(string message, [LiteralExpected] int? duration = null)
=> _dialogController.AddOnScreenMessage(message, duration);

public void ClearGraphics(DisplaySurfaceID? surfaceID = null) => Get2DRenderer(surfaceID).Clear();
public void ClearGraphics(DisplaySurfaceID? surfaceID = null) => LogCallback("the `ClearGraphics()` function has been deprecated; use `Draw()`\n");

public void ClearText() => _displayManager.OSD.ClearGuiText();
public void ClearText() => LogCallback("the `ClearText()` function has been deprecated; use `Draw()`\n");

public void Draw() => _displayManager.DoApiRedraw();

public void AddDrawCallback(Action callback) => _displayManager.OnDraw += callback;

public void RemoveDrawCallback(Action callback) => _displayManager.OnDraw -= callback;

@YoshiRulz YoshiRulz Apr 15, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiHawk already has a draw call batching/scoping method, WithSurface.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's completely unrelated. WithSurface(surfaceId, action) is just a wrapper for UseSurface(surfaceId); action(); UseSurface(originalSurfaceId). The new callbacks don't care about which surface is being drawn, it's just a signal to draw.

@YoshiRulz YoshiRulz Apr 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, I'm saying if you're going to limit draw calls to callbacks, use the callback which already exists and which I've been promoting for this reason. edit: #4799

@SuuperW SuuperW Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how WithSurface could be used in a way that is similar to AddDrawCallback.
And if you were to change how WithSurface works, that would be an API change that I think is better done while changing the name, so that existing code will fail at compile time instead of run time. (better yet, have both so existing code keeps working, at least until the old way is deprecated)

EDIT:

if you're going to limit draw calls to callbacks

This PR does not do that. Old Lua scripts should continue to work exactly as they have before.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You unresolved this conversation. Why? Also I do not see how the PR mention you edited in is relevant to this PR, and I still don't know why you think WithSurface is relevant to this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My WithSurface PR is relevant because it makes the cited changes redundant.
Do not merge this PR before reverting the changes to IGuiApi.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your PR does not address the issue that this PR addresses (the one referred to in the title and the one I explain in OP).

Here's a concrete example of the issue my PR addresses. Take these two Lua scripts that draw on alternating frames:

local y = 100
local function draw()
	-- We want to draw something when some condition is met
	local dummyCondition = emu.framecount() % 2 == 1
	if dummyCondition then
		gui.drawRectangle(10, y, 50, 50, "white", "white")
	else
		-- Since we aren't drawing anything, we have to manually clear.
		gui.clearGraphics()
	end
end
while true do
	draw()
	emu.frameadvance()
end
local y = 10
local function draw()
	-- We want to draw something when some condition is met
	local dummyCondition = emu.framecount() % 2 == 0
	if dummyCondition then
		gui.drawRectangle(10, y, 50, 50, "white", "white")
	else
		-- Since we aren't drawing anything, we have to manually clear.
		gui.clearGraphics()
	end
end
while true do
	draw()
	emu.frameadvance()
end

If you run both of them, you will only see the rectangle from one script. After my PR, gui.clearGraphics() will not work and is also not necessary, so you'll then be able to see both flashing rectangles.

For another example:

y_manual = 10
function draw_manual()
	-- a redraw being triggered by some user action
	-- since there's no new frame, we have to clear
	gui.clearGraphics()
	gui.drawRectangle(100, y_manual, 50, 50, "white", "white")
end
while true do
	draw_manual()
	emu.frameadvance()
end

Using non-local variable and function so that you can use it with the Lua Console text box. When some user action triggers a re-draw, it'll clear the previous graphics and draw again. You cannot ever see the rectangle of either of the first two scripts after manually calling draw_manual(). This will not be automatically fixed in the PR, but a user would see the deprecation warning pointing them to the method they need to use when updating the scripts.


public void SetDefaultForegroundColor(Color color) => _defaultForeground = color;

Expand Down
16 changes: 16 additions & 0 deletions src/BizHawk.Client.Common/Api/Interfaces/IGuiApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,24 @@ public interface IGuiApi : IDisposable, IExternalApi

void AddMessage(string message, int? duration = null);

[Obsolete("use Draw and AddDrawCallback instead")]
void ClearGraphics(DisplaySurfaceID? surfaceID = null);

[Obsolete("use Draw and AddDrawCallback instead")]
void ClearText();

/// <summary>
/// Clears all prior drawings and calls any actions given to <see cref="AddDrawCallback(Action)"/>.
/// This should only be used when you want to redraw while paused.
/// </summary>
void Draw();

/// <summary>
/// The given action will be called before each API draw. This means after each frame and any time <see cref="Draw"/> is called.
/// </summary>
void AddDrawCallback(Action callback);
void RemoveDrawCallback(Action callback);

void SetDefaultForegroundColor(Color color);
void SetDefaultBackgroundColor(Color color);
Color GetDefaultTextBackground();
Expand Down
15 changes: 15 additions & 0 deletions src/BizHawk.Client.Common/DisplayManager/DisplayManagerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public DisplayManagerRenderTargetProvider(Func<Size, IRenderTarget> callback)

public SnowyNullVideo SnowyVP { get; private set; }

public Action/*?*/ OnDraw;

protected DisplayManagerBase(
Config config,
IEmulator emulator,
Expand Down Expand Up @@ -961,12 +963,25 @@ public I2DRenderer GetApiHawk2DRenderer(DisplaySurfaceID surfaceID)
? _apiHawkIDTo2DRenderer.GetValueOrPut(surfaceID, _ => _gl.Create2DRenderer(_imGuiResourceCache))
: throw new ArgumentOutOfRangeException(paramName: nameof(surfaceID), surfaceID, message: "invalid surface ID");

/// <summary>
/// Clear stuff drawn by the gui API
/// </summary>
public void ClearApiHawkSurfaces()
{
foreach (var renderer in _apiHawkIDTo2DRenderer.Values)
{
renderer.Clear();
}
OSD.ClearApiHawkText();
}

/// <summary>
/// Clear stuff drawn by the gui API and raise the draw event.
/// </summary>
public void DoApiRedraw()
{
ClearApiHawkSurfaces();
OnDraw?.Invoke();
}

public void DiscardApiHawkSurfaces()
Expand Down
2 changes: 1 addition & 1 deletion src/BizHawk.Client.Common/DisplayManager/OSDManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void AddGuiText(string message, MessagePosition pos, Color backGround, Co
});
}

public void ClearGuiText()
public void ClearApiHawkText()
=> _guiTextList.Clear();

private void DrawMessage(IBlitter g, UIMessage message, int yOffset)
Expand Down
2 changes: 2 additions & 0 deletions src/BizHawk.Client.Common/lua/NamedLuaFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public sealed class NamedLuaFunction : INamedLuaFunction

public const string EVENT_TYPE_FUTURE = "BeforeFutureFrame";

public const string EVENT_TYPE_DRAW = "OnDraw";

private readonly LuaFunction _function;

private readonly ILuaLibraries _luaImp;
Expand Down
9 changes: 6 additions & 3 deletions src/BizHawk.Client.EmuHawk/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2918,8 +2918,8 @@ private void StepRunLoop_Core(bool force = false)

_lastFastForwardingOrRewinding = isFastForwardingOrRewinding;

// client input-related duties
OSD.ClearGuiText();
// Clear stuff previously drawn by gui API
DisplayManager.ClearApiHawkSurfaces();

CheatList.Pulse();

Expand Down Expand Up @@ -3033,6 +3033,7 @@ private void StepRunLoop_Core(bool force = false)
{
UpdateToolsAfter();
}
DisplayManager.OnDraw?.Invoke();

if (newFrame)
{
Expand Down Expand Up @@ -4080,7 +4081,7 @@ public bool LoadState(string path, string userFriendlyStateName, bool suppressOS
return false;
}

OSD.ClearGuiText();
DisplayManager.ClearApiHawkSurfaces();
if (SavestateLoaded is not null)
{
StateLoadedEventArgs args = new(userFriendlyStateName);
Expand All @@ -4095,6 +4096,8 @@ public bool LoadState(string path, string userFriendlyStateName, bool suppressOS
UpdateToolsLoadstate();
InputManager.AutoFireController.ClearStarts();

DisplayManager.OnDraw?.Invoke(); // not DoApiRedraw, so scripts drawing in the savestate load event still work

//we don't want to analyze how to intermix movies, rewinding, and states
//so purge rewind history when loading a state while doing a movie
if (ToolControllingRewind is null && MovieSession.Movie.IsActive())
Expand Down
29 changes: 26 additions & 3 deletions src/BizHawk.Client.EmuHawk/tools/Lua/Libraries/GuiLuaLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

namespace BizHawk.Client.EmuHawk
{
public sealed class GuiLuaLibrary : LuaLibraryBase, IDisposable
public sealed class GuiLuaLibrary : LuaLibraryBase, IDisposable, IRegisterFunctions
{
private DisplaySurfaceID _rememberedSurfaceID = DisplaySurfaceID.EmuCore;

public NLFAddCallback CreateAndRegisterNamedFunction { get; set; }

public GuiLuaLibrary(ILuaLibraries luaLibsImpl, ApiContainer apiContainer, Action<string> logOutputCallback)
: base(luaLibsImpl, apiContainer, logOutputCallback) {}

Expand Down Expand Up @@ -37,15 +39,36 @@ public void DrawFinish()
public void AddMessage(string message)
=> APIs.Gui.AddMessage(message);

[LuaMethodExample("gui.clearGraphics( );")]
#pragma warning disable CS0618
[LuaDeprecatedMethod]
[LuaMethod("clearGraphics", "clears all lua drawn graphics from the screen")]
public void ClearGraphics(string surfaceName = null)
=> APIs.Gui.ClearGraphics(surfaceID: UseOrFallback(surfaceName));

[LuaMethodExample("gui.cleartext( );")]
[LuaDeprecatedMethod]
[LuaMethod("cleartext", "clears all text created by gui.text()")]
public void ClearText()
=> APIs.Gui.ClearText();
#pragma warning restore CS0618

[LuaMethodExample("gui.draw();")]
[LuaMethod("draw", "Clears all prior drawings and calls any functions given to gui.addDrawCallback. Use this to re-draw while paused.")]
public void Draw()
{
APIs.Gui.Draw();
}

[LuaMethodExample("local draw_cb_id = gui.addDrawCallback(\r\n\tfunction()\r\n\t\t-- gui.draw*** calls go here\r\n\tend);")]
[LuaMethod("addDrawCallback", "The given function will be called before each API draw. This means after each frame and any time gui.draw is called.")]
public string AddDrawCallback(LuaFunction luaf, string name = null)
{
INamedLuaFunction nlf = CreateAndRegisterNamedFunction(luaf, NamedLuaFunction.EVENT_TYPE_DRAW, ApiGroup.PROHIBITED_MID_FRAME, name);
Action callback = () => nlf.Call();
APIs.Gui.AddDrawCallback(callback);
nlf.OnRemove += () => APIs.Gui.RemoveDrawCallback(callback);

return nlf.GuidStr;
}

[LuaMethodExample("gui.defaultForeground( 0x000000FF );")]
[LuaMethod("defaultForeground", "Sets the default foreground color to use in drawing methods, white by default")]
Expand Down
10 changes: 5 additions & 5 deletions src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,12 @@ public LuaConsole()
{
Settings.Columns = LuaListView.AllColumns;

DisplayManager.ClearApiHawkSurfaces();
_openedFiles.StopAllScripts();
DisplayManager.DoApiRedraw();
DisplayManager.ClearApiHawkTextureCache();
ResetDrawSurfacePadding();
ClearFileWatches();
LuaImp?.Close();
DisplayManager.OSD.ClearGuiText();
}
else
{
Expand Down Expand Up @@ -1004,9 +1004,8 @@ private void RemoveScriptMenuItem_Click(object sender, EventArgs e)
}

UpdateDialog();
DisplayManager.ClearApiHawkSurfaces();
DisplayManager.DoApiRedraw();
DisplayManager.ClearApiHawkTextureCache();
DisplayManager.OSD.ClearGuiText();
if (!_openedFiles.Any(static lf => !lf.IsSeparator)) ResetDrawSurfacePadding(); // just removed last script, reset padding
}
}
Expand Down Expand Up @@ -1513,7 +1512,8 @@ protected override bool ProcessTabKey(bool forward)

private void EraseToolbarItem_Click(object sender, EventArgs e)
{
DisplayManager.ClearApiHawkSurfaces();
// tooltip says this is specifically for "stale" or "stuck" stuff
DisplayManager.DoApiRedraw();
}

// Stupid designer
Expand Down
Loading