// MobileGL - MobileGL/MG_Util/Async/ShaderCompilePool.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_Util/Async/JobNode.h>

// This header deliberately includes NO Asio header: asio::thread_pool lives behind the pimpl
// in ShaderCompilePool.cpp. Asio stays a private implementation detail of one translation
// unit, so no consumer target (MG_Test, MG_IntegrationTest, MG_Benchmark - each with its own
// target_include_directories) needs the Asio include path, and no consumer pays its compile
// time. Do not add one here.

namespace MobileGL::MG_Util::Async {
    // Stage 7: on by default. The gate behind the flip (2026-08-09, headless Mesa, both
    // backends): GL30-40 mustpass + KHR-GL46.parallel_shader_compile at async=1 with the
    // extension advertised - 58,344 case-runs, 8 failures, and every one of the 8 also
    // fails standalone at async=0 and under the pre-P1 library, i.e. zero async-attributable
    // deltas. The risk this comment used to name - Iris and Sodium changing their submission
    // schedule the moment GL_KHR_parallel_shader_compile is advertised - remains the one
    // thing a recorded trace cannot cover, which is why the kill switch below stays.
    inline constexpr Bool kAsyncShaderCompileDefault = true;

    // MOBILEGL_ASYNC_SHADER_COMPILE forces the answer either way; unset keeps the built-in
    // default above. Falsy is a complete kill switch: it reverts the threading *and*
    // withdraws GL_KHR_parallel_shader_compile, so the application behaviour change goes
    // with it.
    //
    // This is the pure CONFIGURATION answer, and it is deliberately not affected by
    // glMaxShaderCompilerThreadsKHR: it is what decides whether the extension is advertised
    // at all, and an application that switched threading off through the extension has not
    // made the extension go away. Code deciding whether to enqueue asks
    // AsyncShaderCompileActive() instead.
    Bool AsyncShaderCompileEnabled();

    // ---- GL_KHR_parallel_shader_compile: glMaxShaderCompilerThreadsKHR(count) ----
    // The extension defines count == 0 as "no compiler threads": compilation must happen on
    // the application's thread. That is a mode switch, not a concurrency budget of one, so it
    // is a latch of its own rather than SetMaxConcurrency(1) - a budget of one would still
    // move the work off-thread and still report GL_COMPLETION_STATUS_KHR = GL_FALSE, both of
    // which the extension forbids after a zero count.
    //
    // The latch is process-wide, matching the pool it suspends. It is released by the next
    // nonzero glMaxShaderCompilerThreadsKHR/ARB, which is the only thing that releases it:
    // no implicit re-arm on eglInitialize, on a context switch or at any join, because an
    // application that asked for serial compilation gets to keep it until it asks otherwise.
    void SetAsyncShaderCompileSuspended(Bool suspended);
    Bool IsAsyncShaderCompileSuspended();

    // What every enqueue site branches on: the configuration flag AND the absence of a
    // glMaxShaderCompilerThreadsKHR(0). False makes glCompileShader/glLinkProgram run their
    // bodies inline, exactly as the flag-off path does, which is what makes a subsequent
    // GL_COMPLETION_STATUS_KHR read immediately GL_TRUE.
    Bool AsyncShaderCompileActive();

    // MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS (see Config.h): opt-in, off by default, and a
    // spec violation by design - GL_COMPILE_STATUS and the shader info log answer
    // optimistically while the compile job is in flight instead of joining it. Do not flip
    // this default without an enumerated CTS delta: the compile-error-reporting cases WILL
    // regress under it, deliberately.
    inline constexpr Bool kOptimisticShaderStatusDefault = false;

    // The one question the three optimistic getter sites ask. ANDed with
    // AsyncShaderCompileActive() so that async-off (env kill switch) and
    // glMaxShaderCompilerThreadsKHR(0) both switch the quirk off structurally: in those
    // modes every compile settles before its enqueue returns, so a non-terminal node - the
    // only state the quirk changes - cannot exist, and keeping the AND means there is no
    // new mode interaction to reason about.
    Bool OptimisticShaderStatusActive();

    // min(4, big cores), where a big core is one whose cpufreq ceiling is within 15% of the
    // machine maximum; the whole CPU count where that sysfs tree is absent. Clamped to [1, 4]
    // because peak RSS scales as workers x largest glslang arena, and four
    // Complementary-sized arenas is already the memory ceiling worth accepting on a phone.
    // MOBILEGL_ASYNC_SHADER_COMPILE_THREADS overrides it outright.
    Uint DetectShaderCompileThreadCount();

    class ShaderCompilePool {
    public:
        explicit ShaderCompilePool(Uint threadCount);
        ~ShaderCompilePool();
        ShaderCompilePool(const ShaderCompilePool&) = delete;
        ShaderCompilePool& operator=(const ShaderCompilePool&) = delete;

        // Process-wide pool, leak-at-exit like pGLContext. Sized by
        // DetectShaderCompileThreadCount() on first use; no thread is created until the first
        // Post, so a build that never enables async never starts one.
        static ShaderCompilePool& Get();

        // True only on a thread owned by some ShaderCompilePool. Backs the two asserts that
        // hold the design's invariants up: no GL/EGL reach-back from a worker, and no job
        // body waiting on another job.
        static Bool IsPoolThread();

        // Dispatches the node, or queues it behind the concurrency budget. A stopped pool -
        // and one whose process is exiting - runs the node inline on the calling thread
        // instead, so a late entry point can never resurrect worker threads.
        void Post(SharedPtr<JobNode> node);

        // Cancels everything still queued and joins everything already running. This is the
        // one cancellation path that waits, and it must run before glslang::FinalizeProcess()
        // and before pGLContext is destroyed: in-flight jobs hold their own inputs safely,
        // but they share glslang's process globals, which teardown is about to free.
        void StopAndDrain();

        // StopAndDrain() on the process-wide pool if one was ever created; never creates
        // one. For extra atexit sentinels owned by other subsystems (the SPIR-V validation
        // switch registers one after forcing spirv-tools' lazy function-local tables into
        // existence, so the drain is sequenced before those tables' destructors - a worker
        // mid-Validate would otherwise touch freed memory during process exit).
        static void StopAndDrainProcessPoolAtExit();

        Uint GetThreadCount() const;
        Uint GetMaxConcurrency() const;

        // Bounded concurrency doubles as the memory bound, and is how
        // glMaxShaderCompilerThreadsKHR(n) is honoured: a 300-program pack load cannot put
        // 300 glslang arenas in flight at once. Clamped to [1, thread count].
        void SetMaxConcurrency(Uint n);

    private:
        struct Impl;
        UniquePtr<Impl> m_impl;
    };
} // namespace MobileGL::MG_Util::Async
