mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
Standalone Asio (submodule, asio-1-38-2 @ 8806a680, ASIO_STANDALONE + ASIO_NO_DEPRECATED, header-only - no linked artifact) and the job machinery the async shader pipeline will run on: JobNode (state machine with deferred errors, continuations firing exactly once, dependency counters, cancel semantics split into request vs outcome) and ShaderCompilePool (asio::thread_pool behind a pimpl so no header leaks asio; big-core count via cpufreq at >=85% of peak clamped to [1,4]; lazily constructed, so with the flag off no worker thread ever exists; StopAndDrain leads DestroyImpl). MOBILEGL_ASYNC_SHADER_COMPILE / _THREADS config knobs, default OFF. Nothing in the GL pipeline references the pool yet - grep-verified; the full DirectGLES retrace and compile benches are byte- and time-identical. 25 threaded unit tests, clean across 20x gtest_repeat.
117 lines
5.6 KiB
C++
117 lines
5.6 KiB
C++
// MobileGL - MobileGL/MG_Util/Async/JobNode.h
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#pragma once
|
|
#include <Includes.h>
|
|
#include <MG_Util/Types.h>
|
|
#include <MG_State/GLState/ErrorState/ErrorCode.h>
|
|
#include <MG_State/GLState/ErrorState/ErrorInfo.h>
|
|
|
|
#include <condition_variable>
|
|
|
|
namespace MobileGL::MG_Util::Async {
|
|
enum class JobState : Uint8 {
|
|
Pending, // constructed, not started; may still be sitting in a queue
|
|
Running, // a worker is inside RunBody()
|
|
Complete, // RunBody() returned normally and the node's outputs are readable
|
|
Cancelled, // abandoned before it started, cancelled mid-run, or threw
|
|
};
|
|
|
|
// A GL error a job body wants to raise. Nothing in the compile/link pipeline produces
|
|
// one today (see the design's section 6: GL defines compile/link *failure* as
|
|
// COMPILE_STATUS/LINK_STATUS plus an info log, not as a GL error, which is exactly why
|
|
// asynchronous compilation is legal at all), and JobNode::Finish asserts the vector is
|
|
// still empty in debug builds. The mechanism exists so that the day a body genuinely
|
|
// needs to raise one, the fix is to append here and let the join replay it on the GL
|
|
// thread - not to reach for pGLContext->RecordError() from a worker.
|
|
struct DeferredError {
|
|
Uint64 sequence = 0; // job-global monotonic counter, assigned at record time
|
|
ErrorCode code = ErrorCode::NoError;
|
|
UniquePtr<ErrorInfo> info;
|
|
};
|
|
|
|
struct JobDiagnostics {
|
|
Vector<DeferredError> errors; // replayed, in ascending `sequence`, by the join
|
|
Vector<String> logLines; // worker-side MGLOG text, flushed in order by the join
|
|
};
|
|
|
|
// The scheduling primitive every asynchronous compile and link is built on. A node owns
|
|
// its inputs and its outputs; a worker reads only the former and writes only the latter,
|
|
// which is what makes the "no worker touches GL-thread state" invariant structural
|
|
// rather than review-enforced.
|
|
//
|
|
// State machine, and the only legal transitions:
|
|
// Pending -> Running (a worker picked the node up)
|
|
// Pending -> Cancelled (cancelled before any worker started it)
|
|
// Running -> Complete (RunBody() returned normally)
|
|
// Running -> Cancelled (cancelled mid-run, or RunBody() threw)
|
|
// Complete and Cancelled are terminal and the node is immutable afterwards, so every
|
|
// reader that observed IsTerminal() may read the outputs without further synchronization.
|
|
class JobNode {
|
|
public:
|
|
JobNode() = default;
|
|
virtual ~JobNode() = default;
|
|
JobNode(const JobNode&) = delete;
|
|
JobNode& operator=(const JobNode&) = delete;
|
|
|
|
// Lock-free and non-blocking - safe from any thread, including a pool thread.
|
|
Bool IsTerminal() const;
|
|
Bool IsComplete() const; // Complete only; this is what backs GL_COMPLETION_STATUS_KHR
|
|
Bool IsCancelled() const; // settled AS cancelled - the outcome, not the request
|
|
JobState State() const;
|
|
|
|
// The cancellation *request*, which is what a body polls to bail out early: a
|
|
// running job stays Running until its body returns, so IsCancelled() is still false
|
|
// at that point. Kept separate from IsCancelled() precisely so the two questions
|
|
// ("should I stop?" and "did it end up cancelled?") cannot be confused.
|
|
Bool IsCancellationRequested() const;
|
|
|
|
// Blocks until the node is terminal. GL thread only: a job body that waited on
|
|
// another job could deadlock the whole pool, so this asserts it is not called from a
|
|
// pool thread. Dependencies are resolved by posting late (see ProgramLinkTask::
|
|
// SubmitAfter), never by waiting from inside a body.
|
|
void Wait();
|
|
|
|
// Cooperative and non-blocking. A node that has not started yet goes terminal
|
|
// immediately, so anything waiting on it or chained behind it is released rather
|
|
// than stranded; a running node is flagged and settles as Cancelled when its body
|
|
// returns. Because every node owns its inputs and writes only into itself, an
|
|
// abandoned node is always safe to simply drop - the caller never waits.
|
|
void Cancel();
|
|
|
|
// Runs `fn` once, when this node goes terminal. If the node is ALREADY terminal,
|
|
// `fn` runs on the calling thread before OnTerminal returns. Exactly-once in both
|
|
// directions: the callback is either handed to the finishing thread or run inline,
|
|
// never both.
|
|
void OnTerminal(std::function<void()> fn);
|
|
|
|
// Runs the body on the calling thread. The synchronous path (async disabled,
|
|
// context-less internal shaders, a pool that has been stopped) goes through here, so
|
|
// that "inline" and "on a worker" differ only in which thread executes RunBody().
|
|
void RunInline();
|
|
|
|
JobDiagnostics diagnostics;
|
|
|
|
protected:
|
|
virtual void RunBody() = 0;
|
|
|
|
private:
|
|
friend class ShaderCompilePool;
|
|
|
|
// Pool entry point: cancel check -> RunBody() (exceptions contained) -> Finish().
|
|
void Run();
|
|
Bool TryTransition(JobState from, JobState to);
|
|
|
|
mutable std::mutex m_mutex;
|
|
std::condition_variable m_cv;
|
|
std::atomic<JobState> m_state{JobState::Pending};
|
|
std::atomic<Bool> m_cancelled{false};
|
|
Vector<std::function<void()>> m_continuations;
|
|
};
|
|
} // namespace MobileGL::MG_Util::Async
|