Skip to content

Commit

Permalink
SCExample: Integrate SC::AsyncEventLoop by using SC::AsyncEventLoopMo…
Browse files Browse the repository at this point in the history
…nitor
  • Loading branch information
Pagghiu committed Jun 8, 2024
1 parent 784ffcf commit 5be2d02
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 8 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@
"program": "${workspaceFolder}/_Build/_Outputs/linux-x86_64-make-gcc-Debug/SCExample",
"args": [],
"env": {
"DISPLAY": ":0.0"
"DISPLAY": ":0.0" // If ssh-remoting into a VM you should configure X-auth (or disable it with "xhost +")
},
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build SCExample Debug intel64",
Expand Down Expand Up @@ -263,7 +263,7 @@
"program": "${workspaceFolder}/_Build/_Outputs/linux-arm64-make-gcc-Debug/SCExample",
"args": [],
"env": {
"DISPLAY": ":0.0"
"DISPLAY": ":0.0" // If ssh-remoting into a VM you should configure X-auth (or disable it with "xhost +")
},
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build SCExample Debug arm64",
Expand Down
67 changes: 61 additions & 6 deletions Examples/SCExample/SCExample.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT

// Description: Simple integration of SC::AsyncEventLoop within macOS, windows and linux native GUI event loop.

#include "Libraries/Async/Async.h"
#include "Libraries/Foundation/Deferred.h"
#include "Libraries/Foundation/Result.h"
#include "Libraries/Strings/String.h"
#include "Libraries/Strings/StringBuilder.h"
#include "Libraries/Time/Time.h"
#include "SCExampleSokol.h"
#include "imgui.h"
Expand All @@ -16,7 +21,14 @@ struct ModelData

int pausedCounter = 0;
int continueDrawingForMs = 500;
int timeoutOccursEveryMs = 2000;
int numberOfFrames = 0;

// EventLoop
String loopMessage = "Waiting for first timeout...";
int loopTimeouts = 1;

Time::Milliseconds loopTime;
};

struct ModelBehaviour
Expand All @@ -25,39 +37,78 @@ struct ModelBehaviour

Result create()
{
currentThreadID = Thread::CurrentThreadID();
lastEventTime.snap();
return Result(true);

SC_TRY(eventLoop.create());
timeout.callback = [this](AsyncLoopTimeout::Result& result) { onTimeout(result); };
SC_TRY(timeout.start(eventLoop, Time::Milliseconds(modelData.timeoutOccursEveryMs)));
eventLoopMonitor.onNewEventsAvailable = [this]() { sokol_wake_up(); };
return eventLoopMonitor.create(eventLoop);
}

void resetLastEventTime() { lastEventTime.snap(); }

// This is called during "frame callback" and it will either quickly execute or block when it's time to sleep
Result runLoopStepInsideSokolApp()
{
// Check if we need to pause execution
// Update loop time, mainly to display it in the GUI
modelData.loopTime = eventLoop.getLoopTime().getRelative().inRoundedUpperMilliseconds();

// Check if enough time has passed since last user input event
const Time::Relative sinceLastEvent = Time::HighResolutionCounter().snap().subtractApproximate(lastEventTime);
if (sinceLastEvent.inRoundedUpperMilliseconds() > Time::Milliseconds(modelData.continueDrawingForMs))
{
// Enough time has passed such that we need to pause execution to avoid unnecessary cpu usage
if (modelData.pausedCounter < ModelData::NumPauseFrames)
{
modelData.pausedCounter++;
return Result(true); // one more frame is needed to draw "paused" before pausing for real
return Result(true); // Additional frames are needed to draw "paused" before entering sleep
}
// If we are here we really want to sleep the app until a new input event OR an I/O event arrives
// We implement this logic by:
// 1. Starting to monitor the event loop for IO in a different thread
// 2. Blocking on sokol native gui event loop (sokol_sleep)
// 2a. If input from user occurs, sokol_sleep() will unblock itself
// 2b. If I/O event occurs, calling sokol_wake_up() from the monitoring thread will unblock sokol_sleep
// 3. After returning from sokol_sleep, we make sure to dispatch callbacks for all ready completions
auto resetLastEventTime = MakeDeferred([this] { lastEventTime.snap(); });
SC_TRY(eventLoopMonitor.startMonitoring());
sokol_sleep();
return eventLoopMonitor.stopMonitoringAndDispatchCompletions();
}
else
{
// Keep the application running, but use the occasion to check if some I/O event has been queued by the OS.
// This also updates the loop time, that is needed to fire AsyncLoopTimeouts events with decent precision.
modelData.pausedCounter = 0;
return eventLoop.runNoWait();
}
return Result(true);
}

Result close() { return Result(true); }
Result close()
{
SC_TRY(eventLoopMonitor.close())
return eventLoop.close();
}

private:
AsyncEventLoop eventLoop;
AsyncEventLoopMonitor eventLoopMonitor;
Time::HighResolutionCounter lastEventTime;
AsyncLoopTimeout timeout;

uint64_t currentThreadID = 0;

void onTimeout(AsyncLoopTimeout::Result& result)
{
// The entire point of runStep is to run this callback in the main thread, so let's assert that
SC_ASSERT_RELEASE(currentThreadID == Thread::CurrentThreadID());
(void)StringBuilder(modelData.loopMessage).format("I/O WakeUp {}", modelData.loopTimeouts);
modelData.loopTimeouts++;
result.getAsync().relativeTimeout = Time::Milliseconds(modelData.timeoutOccursEveryMs);
result.reactivateRequest(true);
}
};

struct ApplicationView
Expand All @@ -78,13 +129,17 @@ struct ApplicationView
{
ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Paused");
}
ImGui::Text("%s", modelData.loopMessage.view().bytesIncludingTerminator());
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate,
ImGui::GetIO().Framerate);
ImGui::Text("Frame %d", modelData.numberOfFrames++);
ImGui::Text("Time %.3f", modelData.loopTime.ms / 1000.0f);
ImGui::PushItemWidth(100);
ImGui::InputInt("Continue drawing for (ms)", &modelData.continueDrawingForMs);
ImGui::InputInt("Timeout occurs every (ms)", &modelData.timeoutOccursEveryMs);
ImGui::PopItemWidth();
modelData.continueDrawingForMs = max(0, modelData.continueDrawingForMs);
modelData.timeoutOccursEveryMs = max(0, modelData.timeoutOccursEveryMs);
}
ImGui::End();
}
Expand Down
31 changes: 31 additions & 0 deletions Examples/SCExample/SCExampleSokol.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,34 @@ void sokol_sleep(void)
#error "Unsupported platform"
#endif
}

void sokol_wake_up(void)
{
#if defined(__APPLE__)
NSEvent* event = [NSEvent otherEventWithType: NSEventTypeApplicationDefined
location: NSMakePoint(0,0)
modifierFlags: 0
timestamp: 0.0
windowNumber: 0
context: nil
subtype: 0
data1: 0
data2: 0];
[NSApp postEvent: event atStart: YES];

#elif defined(_WIN32)
HWND hwnd = (HWND)sapp_win32_get_hwnd();
PostMessage(hwnd, WM_USER + 1, 0, 0);
#elif defined(__linux__)
XEvent evt;
_sapp_clear(&evt, sizeof(evt));
evt.xclient.type = ClientMessage;
evt.xclient.message_type = _sapp.x11.WM_STATE;
evt.xclient.format = 32;
evt.xclient.window = _sapp.x11.window;
XSendEvent(_sapp.x11.display, _sapp.x11.window, false, NoEventMask, &evt);
XFlush(_sapp.x11.display);
#else
#error "Unsupported platform"
#endif
}
1 change: 1 addition & 0 deletions Examples/SCExample/SCExampleSokol.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ extern "C"
#endif

void sokol_sleep(void);
void sokol_wake_up(void);

#ifdef __cplusplus
}
Expand Down

0 comments on commit 5be2d02

Please sign in to comment.