Commit 7ae70d8f by jchen10 Committed by Commit Bot

ParallelCompile: Parallelize D3D linking

This adds a new linking state to Program. If a Program is in linking state, on the one hand the foreground thread may continue issuing more GL calls, and on the other hand the background linking threads may be accessing Program internally too. Without a proper constraint there must be conflicts between them. For this purpose, we block any further GL calls to Program until it's actually linked. In addition, we prohibit parallel linking an active program, so that ProgramD3D does not have to worry about such similar conflicts. Also changes the WorkerThread to support limiting the number of concurrently running worker threads. BUG=chromium:849576 Change-Id: I52618647539323f8bf27201320bdf7301c4982e6 Reviewed-on: https://chromium-review.googlesource.com/1127495 Commit-Queue: Jie A Chen <jie.a.chen@intel.com> Reviewed-by: 's avatarJamie Madill <jmadill@chromium.org>
parent ea926a36
......@@ -356,7 +356,8 @@ Context::Context(rx::EGLImplFactory *implFactory,
mDrawFramebufferObserverBinding(this, kDrawFramebufferSubjectIndex),
mReadFramebufferObserverBinding(this, kReadFramebufferSubjectIndex),
mScratchBuffer(1000u),
mZeroFilledBuffer(1000u)
mZeroFilledBuffer(1000u),
mThreadPool(nullptr)
{
// Needed to solve a Clang warning of unused variables.
ANGLE_UNUSED_VARIABLE(mSavedArgsType);
......@@ -577,6 +578,8 @@ egl::Error Context::onDestroy(const egl::Display *display)
mState.mFramebuffers->release(this);
mState.mPipelines->release(this);
mThreadPool.reset();
mImplementation->onDestroy(this);
return egl::NoError();
......@@ -3368,6 +3371,8 @@ void Context::updateCaps()
mValidBufferBindings.set(BufferBinding::DrawIndirect);
mValidBufferBindings.set(BufferBinding::DispatchIndirect);
}
mThreadPool = angle::WorkerThreadPool::Create(mExtensions.parallelShaderCompile);
}
void Context::initWorkarounds()
......@@ -5558,8 +5563,20 @@ void Context::linkProgram(GLuint program)
Program *programObject = getProgram(program);
ASSERT(programObject);
handleError(programObject->link(this));
mGLState.onProgramExecutableChange(programObject);
mStateCache.onProgramExecutableChange(this);
// Don't parallel link a program which is active in any GL contexts. With this assumption, we
// don't need to worry that:
// 1. Draw calls after link use the new executable code or the old one depending on the link
// result.
// 2. When a backend program, e.g., ProgramD3D is linking, other backend classes like
// StateManager11, Renderer11, etc., may have a chance to make unexpected calls to
// ProgramD3D.
if (programObject->isInUse())
{
// isLinked() which forces to resolve linking, will be called.
mGLState.onProgramExecutableChange(programObject);
mStateCache.onProgramExecutableChange(this);
}
}
void Context::releaseShaderCompiler()
......@@ -7551,7 +7568,14 @@ GLenum Context::getConvertedRenderbufferFormat(GLenum internalformat) const
void Context::maxShaderCompilerThreads(GLuint count)
{
GLuint oldCount = mGLState.getMaxShaderCompilerThreads();
mGLState.setMaxShaderCompilerThreads(count);
// A count of zero specifies a request for no parallel compiling or linking.
if ((oldCount == 0 || count == 0) && (oldCount != 0 || count != 0))
{
mThreadPool = angle::WorkerThreadPool::Create(count > 0);
}
mThreadPool->setMaxThreads(count);
}
bool Context::isGLES1() const
......
......@@ -1538,6 +1538,7 @@ class Context final : public egl::LabeledObject, angle::NonCopyable, public angl
// GL_KHR_parallel_shader_compile
void maxShaderCompilerThreads(GLuint count);
std::shared_ptr<angle::WorkerThreadPool> getWorkerThreadPool() const { return mThreadPool; }
const StateCache &getStateCache() const { return mStateCache; }
......@@ -1692,6 +1693,8 @@ class Context final : public egl::LabeledObject, angle::NonCopyable, public angl
// Not really a property of context state. The size and contexts change per-api-call.
mutable angle::ScratchBuffer mScratchBuffer;
mutable angle::ScratchBuffer mZeroFilledBuffer;
std::shared_ptr<angle::WorkerThreadPool> mThreadPool;
};
template <typename T>
......
......@@ -13,6 +13,7 @@
#include "common/angleutils.h"
#include "libANGLE/State.h"
#include "libANGLE/Version.h"
#include "libANGLE/WorkerThread.h"
#include "libANGLE/params.h"
namespace gl
......
......@@ -471,12 +471,12 @@ class Program final : angle::NonCopyable, public LabeledObject
Program(rx::GLImplFactory *factory, ShaderProgramManager *manager, GLuint handle);
void onDestroy(const Context *context);
GLuint id() const { return mHandle; }
GLuint id() const;
void setLabel(const std::string &label) override;
const std::string &getLabel() const override;
rx::ProgramImpl *getImplementation() const { return mProgram; }
rx::ProgramImpl *getImplementation() const;
void attachShader(Shader *shader);
void detachShader(const Context *context, Shader *shader);
......@@ -496,8 +496,15 @@ class Program final : angle::NonCopyable, public LabeledObject
GLint components,
const GLfloat *coeffs);
// KHR_parallel_shader_compile
// Try to link the program asynchrously. As a result, background threads may be launched to
// execute the linking tasks concurrently.
Error link(const Context *context);
bool isLinked() const { return mLinked; }
// Peek whether there is any running linking tasks.
bool isLinking() const;
bool isLinked() const;
bool hasLinkedShaderStage(ShaderType shaderType) const
{
......@@ -536,15 +543,12 @@ class Program final : angle::NonCopyable, public LabeledObject
GLchar *name) const;
GLint getActiveAttributeCount() const;
GLint getActiveAttributeMaxLength() const;
const std::vector<sh::Attribute> &getAttributes() const { return mState.mAttributes; }
const std::vector<sh::Attribute> &getAttributes() const;
GLint getFragDataLocation(const std::string &name) const;
size_t getOutputResourceCount() const;
const std::vector<GLenum> &getOutputVariableTypes() const
{
return mState.mOutputVariableTypes;
}
DrawBufferMask getActiveOutputVariables() const { return mState.mActiveOutputVariables; }
const std::vector<GLenum> &getOutputVariableTypes() const;
DrawBufferMask getActiveOutputVariables() const;
void getActiveUniform(GLuint index,
GLsizei bufsize,
......@@ -558,16 +562,8 @@ class Program final : angle::NonCopyable, public LabeledObject
bool isValidUniformLocation(GLint location) const;
const LinkedUniform &getUniformByLocation(GLint location) const;
const VariableLocation &getUniformLocation(GLint location) const;
const std::vector<VariableLocation> &getUniformLocations() const
{
return mState.mUniformLocations;
}
const LinkedUniform &getUniformByIndex(GLuint index) const
{
ASSERT(index < static_cast<size_t>(mState.mUniforms.size()));
return mState.mUniforms[index];
}
const std::vector<VariableLocation> &getUniformLocations() const;
const LinkedUniform &getUniformByIndex(GLuint index) const;
const BufferVariable &getBufferVariableByIndex(GLuint index) const;
......@@ -678,6 +674,7 @@ class Program final : angle::NonCopyable, public LabeledObject
void addRef();
void release(const Context *context);
unsigned int getRefCount() const;
bool isInUse() const { return getRefCount() != 0; }
void flagForDeletion();
bool isFlaggedForDeletion() const;
......@@ -697,34 +694,16 @@ class Program final : angle::NonCopyable, public LabeledObject
bool isValidated() const;
bool samplesFromTexture(const State &state, GLuint textureID) const;
const AttributesMask &getActiveAttribLocationsMask() const
{
return mState.mActiveAttribLocationsMask;
}
const std::vector<SamplerBinding> &getSamplerBindings() const
{
return mState.mSamplerBindings;
}
const AttributesMask &getActiveAttribLocationsMask() const;
const std::vector<SamplerBinding> &getSamplerBindings() const;
const std::vector<ImageBinding> &getImageBindings() const;
const sh::WorkGroupSize &getComputeShaderLocalSize() const;
PrimitiveMode getGeometryShaderInputPrimitiveType() const;
PrimitiveMode getGeometryShaderOutputPrimitiveType() const;
GLint getGeometryShaderInvocations() const;
GLint getGeometryShaderMaxVertices() const;
const std::vector<ImageBinding> &getImageBindings() const { return mState.mImageBindings; }
const sh::WorkGroupSize &getComputeShaderLocalSize() const
{
return mState.mComputeShaderLocalSize;
}
PrimitiveMode getGeometryShaderInputPrimitiveType() const
{
return mState.mGeometryShaderInputPrimitiveType;
}
PrimitiveMode getGeometryShaderOutputPrimitiveType() const
{
return mState.mGeometryShaderOutputPrimitiveType;
}
GLint getGeometryShaderInvocations() const { return mState.mGeometryShaderInvocations; }
GLint getGeometryShaderMaxVertices() const { return mState.mGeometryShaderMaxVertices; }
const ProgramState &getState() const { return mState; }
const ProgramState &getState() const;
static LinkMismatchError LinkValidateVariablesBase(
const sh::ShaderVariable &variable1,
......@@ -745,21 +724,18 @@ class Program final : angle::NonCopyable, public LabeledObject
const sh::Attribute &getInputResource(GLuint index) const;
const sh::OutputVariable &getOutputResource(GLuint index) const;
const ProgramBindings &getAttributeBindings() const { return mAttributeBindings; }
const ProgramBindings &getUniformLocationBindings() const { return mUniformLocationBindings; }
const ProgramBindings &getFragmentInputBindings() const { return mFragmentInputBindings; }
const ProgramBindings &getAttributeBindings() const;
const ProgramBindings &getUniformLocationBindings() const;
const ProgramBindings &getFragmentInputBindings() const;
int getNumViews() const { return mState.getNumViews(); }
bool usesMultiview() const { return mState.usesMultiview(); }
int getNumViews() const;
bool usesMultiview() const;
ComponentTypeMask getDrawBufferTypeMask() const { return mState.mDrawBufferTypeMask; }
ComponentTypeMask getAttributesTypeMask() const { return mState.mAttributesTypeMask; }
AttributesMask getAttributesMask() const { return mState.mAttributesMask; }
ComponentTypeMask getDrawBufferTypeMask() const;
ComponentTypeMask getAttributesTypeMask() const;
AttributesMask getAttributesMask() const;
const std::vector<GLsizei> &getTransformFeedbackStrides() const
{
return mState.mTransformFeedbackStrides;
}
const std::vector<GLsizei> &getTransformFeedbackStrides() const;
const ActiveTextureMask &getActiveSamplersMask() const { return mState.mActiveSamplersMask; }
......@@ -769,6 +745,8 @@ class Program final : angle::NonCopyable, public LabeledObject
}
private:
struct LinkingState;
~Program() override;
void unlink();
......@@ -857,6 +835,10 @@ class Program final : angle::NonCopyable, public LabeledObject
GLuint getSamplerUniformBinding(const VariableLocation &uniformLocation) const;
bool validateSamplersImpl(InfoLog *infoLog, const Caps &caps);
// Try to resolve linking.
void resolveLink() const;
// Block until linking is finished and resolve it.
void resolveLinkImpl();
ProgramState mState;
rx::ProgramImpl *mProgram;
......@@ -873,6 +855,7 @@ class Program final : angle::NonCopyable, public LabeledObject
ProgramBindings mFragmentInputBindings;
bool mLinked;
std::unique_ptr<LinkingState> mLinkingState;
bool mDeleteStatus; // Flag to indicate that the program can be deleted when no longer in use
unsigned int mRefCount;
......
......@@ -10,148 +10,205 @@
#include "libANGLE/WorkerThread.h"
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
#include <condition_variable>
#include <future>
#include <mutex>
#include <queue>
#include <thread>
#endif // (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
namespace angle
{
namespace priv
{
// SingleThreadedWorkerPool implementation.
SingleThreadedWorkerPool::SingleThreadedWorkerPool(size_t maxThreads)
: WorkerThreadPoolBase(maxThreads)
{
}
WaitableEvent::WaitableEvent() = default;
WaitableEvent::~WaitableEvent() = default;
SingleThreadedWorkerPool::~SingleThreadedWorkerPool()
{
}
WorkerThreadPool::WorkerThreadPool() = default;
WorkerThreadPool::~WorkerThreadPool() = default;
SingleThreadedWaitableEvent SingleThreadedWorkerPool::postWorkerTaskImpl(Closure *task)
class SingleThreadedWaitableEvent final : public WaitableEvent
{
(*task)();
return SingleThreadedWaitableEvent(EventResetPolicy::Automatic, EventInitialState::Signaled);
}
public:
SingleThreadedWaitableEvent() = default;
~SingleThreadedWaitableEvent() = default;
// SingleThreadedWaitableEvent implementation.
SingleThreadedWaitableEvent::SingleThreadedWaitableEvent()
: SingleThreadedWaitableEvent(EventResetPolicy::Automatic, EventInitialState::NonSignaled)
{
}
void wait() override;
bool isReady() override;
};
SingleThreadedWaitableEvent::SingleThreadedWaitableEvent(EventResetPolicy resetPolicy,
EventInitialState initialState)
: WaitableEventBase(resetPolicy, initialState)
void SingleThreadedWaitableEvent::wait()
{
}
SingleThreadedWaitableEvent::~SingleThreadedWaitableEvent()
bool SingleThreadedWaitableEvent::isReady()
{
return true;
}
SingleThreadedWaitableEvent::SingleThreadedWaitableEvent(SingleThreadedWaitableEvent &&other)
: WaitableEventBase(std::move(other))
class SingleThreadedWorkerPool final : public WorkerThreadPool
{
}
public:
std::shared_ptr<WaitableEvent> postWorkerTask(std::shared_ptr<Closure> task) override;
void setMaxThreads(size_t maxThreads) override;
};
SingleThreadedWaitableEvent &SingleThreadedWaitableEvent::operator=(
SingleThreadedWaitableEvent &&other)
{
return copyBase(std::move(other));
}
void SingleThreadedWaitableEvent::resetImpl()
{
mSignaled = false;
}
void SingleThreadedWaitableEvent::waitImpl()
// SingleThreadedWorkerPool implementation.
std::shared_ptr<WaitableEvent> SingleThreadedWorkerPool::postWorkerTask(
std::shared_ptr<Closure> task)
{
(*task)();
return std::make_shared<SingleThreadedWaitableEvent>();
}
void SingleThreadedWaitableEvent::signalImpl()
void SingleThreadedWorkerPool::setMaxThreads(size_t maxThreads)
{
mSignaled = true;
}
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
// AsyncWorkerPool implementation.
AsyncWorkerPool::AsyncWorkerPool(size_t maxThreads) : WorkerThreadPoolBase(maxThreads)
class AsyncWaitableEvent final : public WaitableEvent
{
}
public:
AsyncWaitableEvent() : mIsPending(true) {}
~AsyncWaitableEvent() = default;
AsyncWorkerPool::~AsyncWorkerPool()
{
}
void wait() override;
bool isReady() override;
AsyncWaitableEvent AsyncWorkerPool::postWorkerTaskImpl(Closure *task)
{
auto future = std::async(std::launch::async, [task] { (*task)(); });
private:
friend class AsyncWorkerPool;
void setFuture(std::future<void> &&future);
AsyncWaitableEvent waitable(EventResetPolicy::Automatic, EventInitialState::NonSignaled);
// To block wait() when the task is stil in queue to be run.
// Also to protect the concurrent accesses from both main thread and
// background threads to the member fields.
std::mutex mMutex;
waitable.setFuture(std::move(future));
bool mIsPending;
std::condition_variable mCondition;
std::future<void> mFuture;
};
return waitable;
}
// AsyncWaitableEvent implementation.
AsyncWaitableEvent::AsyncWaitableEvent()
: AsyncWaitableEvent(EventResetPolicy::Automatic, EventInitialState::NonSignaled)
void AsyncWaitableEvent::setFuture(std::future<void> &&future)
{
mFuture = std::move(future);
}
AsyncWaitableEvent::AsyncWaitableEvent(EventResetPolicy resetPolicy, EventInitialState initialState)
: WaitableEventBase(resetPolicy, initialState)
void AsyncWaitableEvent::wait()
{
}
{
std::unique_lock<std::mutex> lock(mMutex);
mCondition.wait(lock, [this] { return !mIsPending; });
}
AsyncWaitableEvent::~AsyncWaitableEvent()
{
ASSERT(mFuture.valid());
mFuture.wait();
}
AsyncWaitableEvent::AsyncWaitableEvent(AsyncWaitableEvent &&other)
: WaitableEventBase(std::move(other)), mFuture(std::move(other.mFuture))
bool AsyncWaitableEvent::isReady()
{
std::lock_guard<std::mutex> lock(mMutex);
if (mIsPending)
{
return false;
}
ASSERT(mFuture.valid());
return mFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
}
AsyncWaitableEvent &AsyncWaitableEvent::operator=(AsyncWaitableEvent &&other)
class AsyncWorkerPool final : public WorkerThreadPool
{
std::swap(mFuture, other.mFuture);
return copyBase(std::move(other));
}
public:
AsyncWorkerPool(size_t maxThreads) : mMaxThreads(maxThreads), mRunningThreads(0){};
~AsyncWorkerPool() = default;
void AsyncWaitableEvent::setFuture(std::future<void> &&future)
{
mFuture = std::move(future);
}
std::shared_ptr<WaitableEvent> postWorkerTask(std::shared_ptr<Closure> task) override;
void setMaxThreads(size_t maxThreads) override;
private:
void checkToRunPendingTasks();
// To protect the concurrent accesses from both main thread and background
// threads to the member fields.
std::mutex mMutex;
size_t mMaxThreads;
size_t mRunningThreads;
std::queue<std::pair<std::shared_ptr<AsyncWaitableEvent>, std::shared_ptr<Closure>>> mTaskQueue;
};
void AsyncWaitableEvent::resetImpl()
// AsyncWorkerPool implementation.
std::shared_ptr<WaitableEvent> AsyncWorkerPool::postWorkerTask(std::shared_ptr<Closure> task)
{
mSignaled = false;
mFuture = std::future<void>();
ASSERT(mMaxThreads > 0);
auto waitable = std::make_shared<AsyncWaitableEvent>();
{
std::lock_guard<std::mutex> lock(mMutex);
mTaskQueue.push(std::make_pair(waitable, task));
}
checkToRunPendingTasks();
return waitable;
}
void AsyncWaitableEvent::waitImpl()
void AsyncWorkerPool::setMaxThreads(size_t maxThreads)
{
if (mSignaled || !mFuture.valid())
{
return;
std::lock_guard<std::mutex> lock(mMutex);
mMaxThreads = (maxThreads == 0xFFFFFFFF ? std::thread::hardware_concurrency() : maxThreads);
}
mFuture.wait();
signal();
checkToRunPendingTasks();
}
void AsyncWaitableEvent::signalImpl()
void AsyncWorkerPool::checkToRunPendingTasks()
{
mSignaled = true;
if (mResetPolicy == EventResetPolicy::Automatic)
std::lock_guard<std::mutex> lock(mMutex);
while (mRunningThreads < mMaxThreads && !mTaskQueue.empty())
{
reset();
auto task = mTaskQueue.front();
mTaskQueue.pop();
auto waitable = task.first;
auto closure = task.second;
auto future = std::async(std::launch::async, [closure, this] {
(*closure)();
{
std::lock_guard<std::mutex> lock(mMutex);
ASSERT(mRunningThreads != 0);
--mRunningThreads;
}
checkToRunPendingTasks();
});
++mRunningThreads;
{
std::lock_guard<std::mutex> waitableLock(waitable->mMutex);
waitable->mIsPending = false;
waitable->setFuture(std::move(future));
}
waitable->mCondition.notify_all();
}
}
#endif // (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
} // namespace priv
// static
std::shared_ptr<WorkerThreadPool> WorkerThreadPool::Create(bool multithreaded)
{
std::shared_ptr<WorkerThreadPool> pool(nullptr);
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
if (multithreaded)
{
pool = std::shared_ptr<WorkerThreadPool>(static_cast<WorkerThreadPool *>(
new AsyncWorkerPool(std::thread::hardware_concurrency())));
}
#endif
if (!pool)
{
return std::shared_ptr<WorkerThreadPool>(
static_cast<WorkerThreadPool *>(new SingleThreadedWorkerPool()));
}
return pool;
}
} // namespace angle
......@@ -12,31 +12,14 @@
#define LIBANGLE_WORKER_THREAD_H_
#include <array>
#include <memory>
#include <vector>
#include "common/debug.h"
#include "libANGLE/features.h"
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
#include <future>
#endif // (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
namespace angle
{
// Indicates whether a WaitableEvent should automatically reset the event state after a single
// waiting thread has been released or remain signaled until reset() is manually invoked.
enum class EventResetPolicy
{
Manual,
Automatic
};
// Specify the initial state on creation.
enum class EventInitialState
{
NonSignaled,
Signaled
};
// A callback function with no return value and no arguments.
class Closure
......@@ -46,239 +29,47 @@ class Closure
virtual void operator()() = 0;
};
namespace priv
{
// An event that we can wait on, useful for joining worker threads.
template <typename Impl>
class WaitableEventBase : angle::NonCopyable
class WaitableEvent : angle::NonCopyable
{
public:
WaitableEventBase(EventResetPolicy resetPolicy, EventInitialState initialState);
WaitableEventBase(WaitableEventBase &&other);
// Puts the event in the un-signaled state.
void reset();
WaitableEvent();
virtual ~WaitableEvent();
// Waits indefinitely for the event to be signaled.
void wait();
// Puts the event in the signaled state, causing any thread blocked on Wait to be woken up.
// The event state is reset to non-signaled after a waiting thread has been released.
void signal();
virtual void wait() = 0;
protected:
Impl &copyBase(Impl &&other);
// Peeks whether the event is ready. If ready, wait() will not block.
virtual bool isReady() = 0;
template <size_t Count>
static size_t WaitManyBase(std::array<Impl, Count> *waitables);
EventResetPolicy mResetPolicy;
bool mSignaled;
};
template <typename Impl>
WaitableEventBase<Impl>::WaitableEventBase(EventResetPolicy resetPolicy,
EventInitialState initialState)
: mResetPolicy(resetPolicy), mSignaled(initialState == EventInitialState::Signaled)
{
}
template <typename Impl>
WaitableEventBase<Impl>::WaitableEventBase(WaitableEventBase &&other)
: mResetPolicy(other.mResetPolicy), mSignaled(other.mSignaled)
{
}
template <typename Impl>
void WaitableEventBase<Impl>::reset()
{
static_cast<Impl *>(this)->resetImpl();
}
template <typename Impl>
void WaitableEventBase<Impl>::wait()
{
static_cast<Impl *>(this)->waitImpl();
}
template <typename Impl>
void WaitableEventBase<Impl>::signal()
{
static_cast<Impl *>(this)->signalImpl();
}
template <typename Impl>
template <size_t Count>
// static
size_t WaitableEventBase<Impl>::WaitManyBase(std::array<Impl, Count> *waitables)
{
ASSERT(Count > 0);
for (size_t index = 0; index < Count; ++index)
static void WaitMany(std::array<std::shared_ptr<WaitableEvent>, Count> *waitables)
{
(*waitables)[index].wait();
ASSERT(Count > 0);
for (size_t index = 0; index < Count; ++index)
{
(*waitables)[index]->wait();
}
}
return 0;
}
template <typename Impl>
Impl &WaitableEventBase<Impl>::copyBase(Impl &&other)
{
std::swap(mSignaled, other.mSignaled);
std::swap(mResetPolicy, other.mResetPolicy);
return *static_cast<Impl *>(this);
}
class SingleThreadedWaitableEvent : public WaitableEventBase<SingleThreadedWaitableEvent>
{
public:
SingleThreadedWaitableEvent();
SingleThreadedWaitableEvent(EventResetPolicy resetPolicy, EventInitialState initialState);
~SingleThreadedWaitableEvent();
SingleThreadedWaitableEvent(SingleThreadedWaitableEvent &&other);
SingleThreadedWaitableEvent &operator=(SingleThreadedWaitableEvent &&other);
void resetImpl();
void waitImpl();
void signalImpl();
// Wait, synchronously, on multiple events.
// returns the index of a WaitableEvent which has been signaled.
template <size_t Count>
static size_t WaitMany(std::array<SingleThreadedWaitableEvent, Count> *waitables);
};
template <size_t Count>
// static
size_t SingleThreadedWaitableEvent::WaitMany(
std::array<SingleThreadedWaitableEvent, Count> *waitables)
{
return WaitableEventBase<SingleThreadedWaitableEvent>::WaitManyBase(waitables);
}
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
class AsyncWaitableEvent : public WaitableEventBase<AsyncWaitableEvent>
{
public:
AsyncWaitableEvent();
AsyncWaitableEvent(EventResetPolicy resetPolicy, EventInitialState initialState);
~AsyncWaitableEvent();
AsyncWaitableEvent(AsyncWaitableEvent &&other);
AsyncWaitableEvent &operator=(AsyncWaitableEvent &&other);
void resetImpl();
void waitImpl();
void signalImpl();
// Wait, synchronously, on multiple events.
// returns the index of a WaitableEvent which has been signaled.
template <size_t Count>
static size_t WaitMany(std::array<AsyncWaitableEvent, Count> *waitables);
private:
friend class AsyncWorkerPool;
void setFuture(std::future<void> &&future);
std::future<void> mFuture;
};
template <size_t Count>
// static
size_t AsyncWaitableEvent::WaitMany(std::array<AsyncWaitableEvent, Count> *waitables)
{
return WaitableEventBase<AsyncWaitableEvent>::WaitManyBase(waitables);
}
#endif // (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
// The traits class allows the the thread pool to return the "Typed" waitable event from postTask.
// Otherwise postTask would always think it returns the current active type, so the unit tests
// could not run on multiple worker types in the same compilation.
template <typename Impl>
struct WorkerThreadPoolTraits;
class SingleThreadedWorkerPool;
template <>
struct WorkerThreadPoolTraits<SingleThreadedWorkerPool>
{
using WaitableEventType = SingleThreadedWaitableEvent;
};
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
class AsyncWorkerPool;
template <>
struct WorkerThreadPoolTraits<AsyncWorkerPool>
{
using WaitableEventType = AsyncWaitableEvent;
};
#endif // (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
// Request WorkerThreads from the WorkerThreadPool. Each pool can keep worker threads around so
// we avoid the costly spin up and spin down time.
template <typename Impl>
class WorkerThreadPoolBase : angle::NonCopyable
class WorkerThreadPool : angle::NonCopyable
{
public:
WorkerThreadPoolBase(size_t maxThreads);
~WorkerThreadPoolBase();
WorkerThreadPool();
virtual ~WorkerThreadPool();
using WaitableEventType = typename WorkerThreadPoolTraits<Impl>::WaitableEventType;
static std::shared_ptr<WorkerThreadPool> Create(bool multithreaded);
// Returns an event to wait on for the task to finish.
// If the pool fails to create the task, returns null.
WaitableEventType postWorkerTask(Closure *task);
};
template <typename Impl>
WorkerThreadPoolBase<Impl>::WorkerThreadPoolBase(size_t maxThreads)
{
}
template <typename Impl>
WorkerThreadPoolBase<Impl>::~WorkerThreadPoolBase()
{
}
template <typename Impl>
typename WorkerThreadPoolBase<Impl>::WaitableEventType WorkerThreadPoolBase<Impl>::postWorkerTask(
Closure *task)
{
return static_cast<Impl *>(this)->postWorkerTaskImpl(task);
}
class SingleThreadedWorkerPool : public WorkerThreadPoolBase<SingleThreadedWorkerPool>
{
public:
SingleThreadedWorkerPool(size_t maxThreads);
~SingleThreadedWorkerPool();
virtual std::shared_ptr<WaitableEvent> postWorkerTask(std::shared_ptr<Closure> task) = 0;
SingleThreadedWaitableEvent postWorkerTaskImpl(Closure *task);
virtual void setMaxThreads(size_t maxThreads) = 0;
};
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
class AsyncWorkerPool : public WorkerThreadPoolBase<AsyncWorkerPool>
{
public:
AsyncWorkerPool(size_t maxThreads);
~AsyncWorkerPool();
AsyncWaitableEvent postWorkerTaskImpl(Closure *task);
};
#endif // (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
} // namespace priv
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
using WaitableEvent = priv::AsyncWaitableEvent;
using WorkerThreadPool = priv::AsyncWorkerPool;
#else
using WaitableEvent = priv::SingleThreadedWaitableEvent;
using WorkerThreadPool = priv::SingleThreadedWorkerPool;
#endif // (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
} // namespace angle
#endif // LIBANGLE_WORKER_THREAD_H_
......@@ -16,23 +16,8 @@ using namespace angle;
namespace
{
template <typename T>
class WorkerPoolTest : public ::testing::Test
{
public:
T workerPool = {4};
};
#if (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
using WorkerPoolTypes = ::testing::Types<priv::AsyncWorkerPool, priv::SingleThreadedWorkerPool>;
#else
using WorkerPoolTypes = ::testing::Types<priv::SingleThreadedWorkerPool>;
#endif // (ANGLE_STD_ASYNC_WORKERS == ANGLE_ENABLED)
TYPED_TEST_CASE(WorkerPoolTest, WorkerPoolTypes);
// Tests simple worker pool application.
TYPED_TEST(WorkerPoolTest, SimpleTask)
TEST(WorkerPoolTest, SimpleTask)
{
class TestTask : public Closure
{
......@@ -42,17 +27,23 @@ TYPED_TEST(WorkerPoolTest, SimpleTask)
bool fired = false;
};
std::array<TestTask, 4> tasks;
std::array<typename TypeParam::WaitableEventType, 4> waitables = {{
this->workerPool.postWorkerTask(&tasks[0]), this->workerPool.postWorkerTask(&tasks[1]),
this->workerPool.postWorkerTask(&tasks[2]), this->workerPool.postWorkerTask(&tasks[3]),
}};
TypeParam::WaitableEventType::WaitMany(&waitables);
for (const auto &task : tasks)
std::array<std::shared_ptr<WorkerThreadPool>, 2> pools = {
{WorkerThreadPool::Create(false), WorkerThreadPool::Create(true)}};
for (auto &pool : pools)
{
EXPECT_TRUE(task.fired);
std::array<std::shared_ptr<TestTask>, 4> tasks = {
{std::make_shared<TestTask>(), std::make_shared<TestTask>(),
std::make_shared<TestTask>(), std::make_shared<TestTask>()}};
std::array<std::shared_ptr<WaitableEvent>, 4> waitables = {
{pool->postWorkerTask(tasks[0]), pool->postWorkerTask(tasks[1]),
pool->postWorkerTask(tasks[2]), pool->postWorkerTask(tasks[3])}};
WaitableEvent::WaitMany(&waitables);
for (const auto &task : tasks)
{
EXPECT_TRUE(task->fired);
}
}
}
......
......@@ -1019,6 +1019,9 @@ void QueryProgramiv(const Context *context, const Program *program, GLenum pname
case GL_LINK_STATUS:
*params = program->isLinked();
return;
case GL_COMPLETION_STATUS_KHR:
*params = program->isLinking() ? GL_FALSE : GL_TRUE;
return;
case GL_VALIDATE_STATUS:
*params = program->isValidated();
return;
......@@ -1162,6 +1165,11 @@ void QueryShaderiv(const Context *context, Shader *shader, GLenum pname, GLint *
case GL_COMPILE_STATUS:
*params = shader->isCompiled(context) ? GL_TRUE : GL_FALSE;
return;
case GL_COMPLETION_STATUS_KHR:
// TODO(jie.a.chen@intel.com): Parallelize shader compilation.
// http://crbug.com/849576
*params = shader->isCompiled(context) ? GL_TRUE : GL_FALSE;
return;
case GL_INFO_LOG_LENGTH:
*params = shader->getInfoLogLength(context);
return;
......
......@@ -30,6 +30,35 @@ struct BlockMemberInfo;
namespace rx
{
// Provides a mechanism to access the result of asynchronous linking.
class LinkEvent : angle::NonCopyable
{
public:
virtual ~LinkEvent(){};
// Please be aware that these methods may be called under a gl::Context other
// than the one where the LinkEvent was created.
//
// Waits until the linking is actually done. Returns true if the linking
// succeeded, false otherwise.
virtual bool wait() = 0;
// Peeks whether the linking is still ongoing.
virtual bool isLinking() = 0;
};
// Wraps an already done linking.
class LinkEventDone final : public LinkEvent
{
public:
LinkEventDone(const gl::LinkResult &result) : mResult(result) {}
bool wait() override { return (!mResult.isError() && mResult.getResult()); }
bool isLinking() override { return false; }
private:
gl::LinkResult mResult;
};
class ProgramImpl : angle::NonCopyable
{
public:
......@@ -44,9 +73,9 @@ class ProgramImpl : angle::NonCopyable
virtual void setBinaryRetrievableHint(bool retrievable) = 0;
virtual void setSeparable(bool separable) = 0;
virtual gl::LinkResult link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) = 0;
virtual std::unique_ptr<LinkEvent> link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) = 0;
virtual GLboolean validate(const gl::Caps &caps, gl::InfoLog *infoLog) = 0;
virtual void setUniform1fv(GLint location, GLsizei count, const GLfloat *v) = 0;
......
......@@ -196,9 +196,9 @@ class ProgramD3D : public ProgramImpl
ShaderExecutableD3D **outExectuable,
gl::InfoLog *infoLog);
angle::Result getComputeExecutable(ShaderExecutableD3D **outExecutable);
gl::LinkResult link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) override;
std::unique_ptr<LinkEvent> link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) override;
GLboolean validate(const gl::Caps &caps, gl::InfoLog *infoLog) override;
void setPathFragmentInputGen(const std::string &inputName,
......@@ -311,6 +311,7 @@ class ProgramD3D : public ProgramImpl
class GetVertexExecutableTask;
class GetPixelExecutableTask;
class GetGeometryExecutableTask;
class GraphicsProgramLinkEvent;
class VertexExecutable
{
......@@ -451,7 +452,8 @@ class ProgramD3D : public ProgramImpl
GLboolean transpose,
const GLfloat *value);
gl::LinkResult compileProgramExecutables(const gl::Context *context, gl::InfoLog &infoLog);
std::unique_ptr<LinkEvent> compileProgramExecutables(const gl::Context *context,
gl::InfoLog &infoLog);
gl::LinkResult compileComputeExecutable(const gl::Context *context, gl::InfoLog &infoLog);
void gatherTransformFeedbackVaryings(const gl::VaryingPacking &varyings,
......
......@@ -39,8 +39,7 @@ RendererD3D::RendererD3D(egl::Display *display)
mCapsInitialized(false),
mWorkaroundsInitialized(false),
mDisjoint(false),
mDeviceLost(false),
mWorkerThreadPool(4)
mDeviceLost(false)
{
}
......@@ -171,11 +170,6 @@ const gl::Limitations &RendererD3D::getNativeLimitations() const
return mNativeLimitations;
}
angle::WorkerThreadPool *RendererD3D::getWorkerThreadPool()
{
return &mWorkerThreadPool;
}
Serial RendererD3D::generateSerial()
{
return mSerialFactory.generate();
......
......@@ -17,7 +17,6 @@
#include "libANGLE/ContextState.h"
#include "libANGLE/Device.h"
#include "libANGLE/Version.h"
#include "libANGLE/WorkerThread.h"
#include "libANGLE/angletypes.h"
#include "libANGLE/formatutils.h"
#include "libANGLE/renderer/d3d/VertexDataManager.h"
......@@ -384,8 +383,6 @@ class RendererD3D : public BufferFactoryD3D
angle::Result initRenderTarget(const gl::Context *context, RenderTargetD3D *renderTarget);
angle::WorkerThreadPool *getWorkerThreadPool();
virtual angle::Result getIncompleteTexture(const gl::Context *context,
gl::TextureType type,
gl::Texture **textureOut) = 0;
......@@ -427,8 +424,6 @@ class RendererD3D : public BufferFactoryD3D
bool mDisjoint;
bool mDeviceLost;
angle::WorkerThreadPool mWorkerThreadPool;
SerialFactory mSerialFactory;
};
......
......@@ -125,9 +125,17 @@ void ProgramGL::setSeparable(bool separable)
mFunctions->programParameteri(mProgramID, GL_PROGRAM_SEPARABLE, separable ? GL_TRUE : GL_FALSE);
}
gl::LinkResult ProgramGL::link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog)
std::unique_ptr<LinkEvent> ProgramGL::link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog)
{
// TODO(jie.a.chen@intel.com): Parallelize linking.
return std::make_unique<LinkEventDone>(linkImpl(context, resources, infoLog));
}
gl::LinkResult ProgramGL::linkImpl(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog)
{
preLink();
......
......@@ -38,9 +38,9 @@ class ProgramGL : public ProgramImpl
void setBinaryRetrievableHint(bool retrievable) override;
void setSeparable(bool separable) override;
gl::LinkResult link(const gl::Context *contextImpl,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) override;
std::unique_ptr<LinkEvent> link(const gl::Context *contextImpl,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) override;
GLboolean validate(const gl::Caps &caps, gl::InfoLog *infoLog) override;
void setUniform1fv(GLint location, GLsizei count, const GLfloat *v) override;
......@@ -88,6 +88,10 @@ class ProgramGL : public ProgramImpl
void preLink();
bool checkLinkStatus(gl::InfoLog &infoLog);
void postLink();
gl::LinkResult linkImpl(const gl::Context *contextImpl,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog);
void reapplyUBOBindingsIfNeeded(const gl::Context *context);
bool getUniformBlockSize(const std::string &blockName,
......
......@@ -41,11 +41,11 @@ void ProgramNULL::setSeparable(bool separable)
{
}
gl::LinkResult ProgramNULL::link(const gl::Context *contextImpl,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog)
std::unique_ptr<LinkEvent> ProgramNULL::link(const gl::Context *contextImpl,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog)
{
return true;
return std::make_unique<LinkEventDone>(true);
}
GLboolean ProgramNULL::validate(const gl::Caps &caps, gl::InfoLog *infoLog)
......
......@@ -28,9 +28,9 @@ class ProgramNULL : public ProgramImpl
void setBinaryRetrievableHint(bool retrievable) override;
void setSeparable(bool separable) override;
gl::LinkResult link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) override;
std::unique_ptr<LinkEvent> link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) override;
GLboolean validate(const gl::Caps &caps, gl::InfoLog *infoLog) override;
void setUniform1fv(GLint location, GLsizei count, const GLfloat *v) override;
......
......@@ -250,9 +250,18 @@ void ProgramVk::setSeparable(bool separable)
UNIMPLEMENTED();
}
gl::LinkResult ProgramVk::link(const gl::Context *glContext,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog)
std::unique_ptr<LinkEvent> ProgramVk::link(const gl::Context *glContext,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog)
{
// TODO(jie.a.chen@intel.com): Parallelize linking.
// http://crbug.com/849576
return std::make_unique<LinkEventDone>(linkImpl(glContext, resources, infoLog));
}
gl::LinkResult ProgramVk::linkImpl(const gl::Context *glContext,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog)
{
ContextVk *contextVk = vk::GetImpl(glContext);
RendererVk *renderer = contextVk->getRenderer();
......
......@@ -32,9 +32,9 @@ class ProgramVk : public ProgramImpl
void setBinaryRetrievableHint(bool retrievable) override;
void setSeparable(bool separable) override;
gl::LinkResult link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) override;
std::unique_ptr<LinkEvent> link(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog) override;
GLboolean validate(const gl::Caps &caps, gl::InfoLog *infoLog) override;
void setUniform1fv(GLint location, GLsizei count, const GLfloat *v) override;
......@@ -136,6 +136,9 @@ class ProgramVk : public ProgramImpl
template <typename T>
void setUniformImpl(GLint location, GLsizei count, const T *v, GLenum entryPointType);
gl::LinkResult linkImpl(const gl::Context *context,
const gl::ProgramLinkedResources &resources,
gl::InfoLog &infoLog);
// State for the default uniform blocks.
struct DefaultUniformBlock final : private angle::NonCopyable
......
......@@ -4368,6 +4368,7 @@ bool ValidateGetProgramivBase(Context *context, GLuint program, GLenum pname, GL
{
case GL_DELETE_STATUS:
case GL_LINK_STATUS:
case GL_COMPLETION_STATUS_KHR:
case GL_VALIDATE_STATUS:
case GL_INFO_LOG_LENGTH:
case GL_ATTACHED_SHADERS:
......@@ -5309,6 +5310,7 @@ bool ValidateGetShaderivBase(Context *context, GLuint shader, GLenum pname, GLsi
case GL_SHADER_TYPE:
case GL_DELETE_STATUS:
case GL_COMPILE_STATUS:
case GL_COMPLETION_STATUS_KHR:
case GL_INFO_LOG_LENGTH:
case GL_SHADER_SOURCE_LENGTH:
break;
......
......@@ -8,6 +8,8 @@
#include "test_utils/ANGLETest.h"
#include "random_utils.h"
using namespace angle;
namespace
......@@ -43,6 +45,59 @@ class ParallelShaderCompileTest : public ANGLETest
}
return true;
}
class ClearColorWithDraw
{
public:
ClearColorWithDraw(GLubyte color) : mColor(color, color, color, 255) {}
bool compileAndLink()
{
mProgram =
CompileProgramParallel(insertRandomString(essl1_shaders::vs::Simple()),
insertRandomString(essl1_shaders::fs::UniformColor()));
return (mProgram != 0);
}
bool isLinkCompleted()
{
GLint status;
glGetProgramiv(mProgram, GL_COMPLETION_STATUS_KHR, &status);
return (status == GL_TRUE);
}
void drawAndVerify(ParallelShaderCompileTest *test)
{
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);
glUseProgram(mProgram);
ASSERT_GL_NO_ERROR();
GLint colorUniformLocation =
glGetUniformLocation(mProgram, essl1_shaders::ColorUniform());
ASSERT_NE(colorUniformLocation, -1);
auto normalizeColor = mColor.toNormalizedVector();
glUniform4fv(colorUniformLocation, 1, normalizeColor.data());
test->drawQuad(mProgram, essl1_shaders::PositionAttrib(), 0.5f);
EXPECT_PIXEL_COLOR_EQ(test->getWindowWidth() / 2, test->getWindowHeight() / 2, mColor);
glUseProgram(0);
glDeleteProgram(mProgram);
ASSERT_GL_NO_ERROR();
}
private:
std::string insertRandomString(const std::string &source)
{
RNG rng;
std::ostringstream ostream;
ostream << "// Random string to fool program cache: " << rng.randomInt() << "\n"
<< source;
return ostream.str();
}
GLColor mColor;
GLuint mProgram;
};
};
// Test basic functionality of GL_KHR_parallel_shader_compile
......@@ -58,6 +113,38 @@ TEST_P(ParallelShaderCompileTest, Basic)
EXPECT_EQ(8, count);
}
// Test to compile and link many programs in parallel.
TEST_P(ParallelShaderCompileTest, LinkAndDrawManyPrograms)
{
ANGLE_SKIP_TEST_IF(!ensureParallelShaderCompileExtensionAvailable());
std::vector<std::unique_ptr<ClearColorWithDraw>> tasks;
constexpr int kTaskCount = 32;
for (int i = 0; i < kTaskCount; ++i)
{
std::unique_ptr<ClearColorWithDraw> task(new ClearColorWithDraw(i * 255 / kTaskCount));
bool isLinking = task->compileAndLink();
ASSERT_TRUE(isLinking);
tasks.push_back(std::move(task));
}
constexpr unsigned int kPollInterval = 100;
while (!tasks.empty())
{
for (unsigned int i = 0; i < tasks.size();)
{
auto &task = tasks[i];
if (task->isLinkCompleted())
{
task->drawAndVerify(this);
tasks.erase(tasks.begin() + i);
continue;
}
++i;
}
Sleep(kPollInterval);
}
}
ANGLE_INSTANTIATE_TEST(ParallelShaderCompileTest,
ES2_D3D9(),
ES2_D3D11(),
......
......@@ -126,12 +126,11 @@ GLuint CompileProgramWithTransformFeedback(
transformFeedbackVaryings, bufferMode);
}
GLuint CompileProgramWithGSAndTransformFeedback(
const std::string &vsSource,
const std::string &gsSource,
const std::string &fsSource,
const std::vector<std::string> &transformFeedbackVaryings,
GLenum bufferMode)
static GLuint CompileAndLinkProgram(const std::string &vsSource,
const std::string &gsSource,
const std::string &fsSource,
const std::vector<std::string> &transformFeedbackVaryings,
GLenum bufferMode)
{
GLuint program = glCreateProgram();
......@@ -182,6 +181,22 @@ GLuint CompileProgramWithGSAndTransformFeedback(
glLinkProgram(program);
return program;
}
GLuint CompileProgramWithGSAndTransformFeedback(
const std::string &vsSource,
const std::string &gsSource,
const std::string &fsSource,
const std::vector<std::string> &transformFeedbackVaryings,
GLenum bufferMode)
{
GLuint program =
CompileAndLinkProgram(vsSource, gsSource, fsSource, transformFeedbackVaryings, bufferMode);
if (program == 0)
{
return 0;
}
return CheckLinkStatusAndReturnProgram(program, true);
}
......@@ -190,6 +205,12 @@ GLuint CompileProgram(const std::string &vsSource, const std::string &fsSource)
return CompileProgramWithGS(vsSource, "", fsSource);
}
GLuint CompileProgramParallel(const std::string &vsSource, const std::string &fsSource)
{
std::vector<std::string> emptyVector;
return CompileAndLinkProgram(vsSource, "", fsSource, emptyVector, GL_NONE);
}
GLuint CompileProgramWithGS(const std::string &vsSource,
const std::string &gsSource,
const std::string &fsSource)
......
......@@ -34,6 +34,9 @@ CompileProgramWithGSAndTransformFeedback(const std::string &vsSource,
const std::vector<std::string> &transformFeedbackVaryings,
GLenum bufferMode);
ANGLE_EXPORT GLuint CompileProgram(const std::string &vsSource, const std::string &fsSource);
ANGLE_EXPORT GLuint CompileProgramParallel(const std::string &vsSource,
const std::string &fsSource);
ANGLE_EXPORT GLuint CompileProgramWithGS(const std::string &vsSource,
const std::string &gsSource,
const std::string &fsSource);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment