зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1826204 - Refactor RenderThread's RenderNotifier event handling r=gfx-reviewers,lsalzman
Refactor RenderThread's RenderNotifier event handling for Bug 1804233. Bug 1804233 is going to add deferring processing of RenderNotifier events if RemoteTexture is not ready. For handling it, RenderNotifier events are processed by using WrNotifierEvent queue. Differential Revision: https://phabricator.services.mozilla.com/D174524
This commit is contained in:
Родитель
7d39526f2e
Коммит
8576364e3e
|
@ -310,6 +310,8 @@ void RenderThread::AddRenderer(wr::WindowId aWindowId,
|
|||
|
||||
auto windows = mWindowInfos.Lock();
|
||||
windows->emplace(AsUint64(aWindowId), new WindowInfo());
|
||||
mWrNotifierEventsQueues.emplace(AsUint64(aWindowId),
|
||||
new std::queue<WrNotifierEvent>);
|
||||
}
|
||||
|
||||
void RenderThread::RemoveRenderer(wr::WindowId aWindowId) {
|
||||
|
@ -338,6 +340,19 @@ void RenderThread::RemoveRenderer(wr::WindowId aWindowId) {
|
|||
auto it = windows->find(AsUint64(aWindowId));
|
||||
MOZ_ASSERT(it != windows->end());
|
||||
windows->erase(it);
|
||||
|
||||
// Defer std::deque<WrNotifierEvent> remove, RemoveRenderer() is called in
|
||||
// HandleWrNotifierEvents().
|
||||
RefPtr<Runnable> runnable =
|
||||
NS_NewRunnableFunction("RenderThread::RemoveRenderer", [aWindowId]() {
|
||||
auto* self = RenderThread::Get();
|
||||
auto it = self->mWrNotifierEventsQueues.find(AsUint64(aWindowId));
|
||||
if (it == self->mWrNotifierEventsQueues.end()) {
|
||||
return;
|
||||
}
|
||||
self->mWrNotifierEventsQueues.erase(it);
|
||||
});
|
||||
RenderThread::Get()->PostRunnable(runnable.forget());
|
||||
}
|
||||
|
||||
RendererOGL* RenderThread::GetRenderer(wr::WindowId aWindowId) {
|
||||
|
@ -369,6 +384,167 @@ size_t RenderThread::ActiveRendererCount() const {
|
|||
return num_active;
|
||||
}
|
||||
|
||||
void RenderThread::WrNotifierEvent_WakeUp(WrWindowId aWindowId,
|
||||
bool aCompositeNeeded) {
|
||||
auto windows = mWindowInfos.Lock();
|
||||
auto it = windows->find(AsUint64(aWindowId));
|
||||
if (it == windows->end()) {
|
||||
MOZ_ASSERT(false);
|
||||
return;
|
||||
}
|
||||
|
||||
WindowInfo* info = it->second.get();
|
||||
|
||||
info->mPendingWrNotifierEvents.emplace(
|
||||
WrNotifierEvent::WakeUp(aCompositeNeeded));
|
||||
PostWrNotifierEvents(aWindowId, info);
|
||||
}
|
||||
|
||||
void RenderThread::WrNotifierEvent_NewFrameReady(WrWindowId aWindowId,
|
||||
bool aCompositeNeeded,
|
||||
FramePublishId aPublishId) {
|
||||
auto windows = mWindowInfos.Lock();
|
||||
auto it = windows->find(AsUint64(aWindowId));
|
||||
if (it == windows->end()) {
|
||||
MOZ_ASSERT(false);
|
||||
return;
|
||||
}
|
||||
WindowInfo* info = it->second.get();
|
||||
|
||||
info->mPendingWrNotifierEvents.emplace(
|
||||
WrNotifierEvent::NewFrameReady(aCompositeNeeded, aPublishId));
|
||||
PostWrNotifierEvents(aWindowId, info);
|
||||
}
|
||||
|
||||
void RenderThread::WrNotifierEvent_ExternalEvent(WrWindowId aWindowId,
|
||||
size_t aRawEvent) {
|
||||
UniquePtr<RendererEvent> evt(reinterpret_cast<RendererEvent*>(aRawEvent));
|
||||
{
|
||||
auto windows = mWindowInfos.Lock();
|
||||
auto it = windows->find(AsUint64(aWindowId));
|
||||
if (it == windows->end()) {
|
||||
MOZ_ASSERT(false);
|
||||
return;
|
||||
}
|
||||
WindowInfo* info = it->second.get();
|
||||
|
||||
info->mPendingWrNotifierEvents.emplace(
|
||||
WrNotifierEvent::ExternalEvent(std::move(evt)));
|
||||
PostWrNotifierEvents(aWindowId, info);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderThread::PostWrNotifierEvents(WrWindowId aWindowId) {
|
||||
{
|
||||
auto windows = mWindowInfos.Lock();
|
||||
auto it = windows->find(AsUint64(aWindowId));
|
||||
if (it == windows->end()) {
|
||||
MOZ_ASSERT(false);
|
||||
return;
|
||||
}
|
||||
WindowInfo* info = it->second.get();
|
||||
PostWrNotifierEvents(aWindowId, info);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderThread::PostWrNotifierEvents(WrWindowId aWindowId,
|
||||
WindowInfo* aInfo) {
|
||||
// Runnable has already been triggered.
|
||||
if (aInfo->mWrNotifierEventsRunnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Runnable has not been triggered yet.
|
||||
RefPtr<nsIRunnable> runnable = NewRunnableMethod<WrWindowId>(
|
||||
"RenderThread::HandleWrNotifierEvents", this,
|
||||
&RenderThread::HandleWrNotifierEvents, aWindowId);
|
||||
aInfo->mWrNotifierEventsRunnable = runnable;
|
||||
PostRunnable(runnable.forget());
|
||||
}
|
||||
|
||||
void RenderThread::HandleWrNotifierEvents(WrWindowId aWindowId) {
|
||||
MOZ_ASSERT(IsInRenderThread());
|
||||
|
||||
auto eventsIt = mWrNotifierEventsQueues.find(AsUint64(aWindowId));
|
||||
if (eventsIt == mWrNotifierEventsQueues.end()) {
|
||||
return;
|
||||
}
|
||||
auto* events = eventsIt->second.get();
|
||||
|
||||
{
|
||||
auto windows = mWindowInfos.Lock();
|
||||
auto infoIt = windows->find(AsUint64(aWindowId));
|
||||
if (infoIt == windows->end()) {
|
||||
MOZ_ASSERT(false);
|
||||
return;
|
||||
}
|
||||
WindowInfo* info = infoIt->second.get();
|
||||
info->mWrNotifierEventsRunnable = nullptr;
|
||||
|
||||
if (events->empty() && !info->mPendingWrNotifierEvents.empty()) {
|
||||
events->swap(info->mPendingWrNotifierEvents);
|
||||
}
|
||||
}
|
||||
|
||||
bool handleNext = true;
|
||||
|
||||
while (!events->empty() && handleNext) {
|
||||
auto& front = events->front();
|
||||
switch (front.mTag) {
|
||||
case WrNotifierEvent::Tag::WakeUp:
|
||||
WrNotifierEvent_HandleWakeUp(aWindowId, front.CompositeNeeded());
|
||||
handleNext = false;
|
||||
break;
|
||||
case WrNotifierEvent::Tag::NewFrameReady:
|
||||
WrNotifierEvent_HandleNewFrameReady(aWindowId, front.CompositeNeeded(),
|
||||
front.PublishId());
|
||||
handleNext = false;
|
||||
break;
|
||||
case WrNotifierEvent::Tag::ExternalEvent:
|
||||
WrNotifierEvent_HandleExternalEvent(aWindowId, front.ExternalEvent());
|
||||
break;
|
||||
}
|
||||
events->pop();
|
||||
}
|
||||
|
||||
{
|
||||
auto windows = mWindowInfos.Lock();
|
||||
auto it = windows->find(AsUint64(aWindowId));
|
||||
if (it == windows->end()) {
|
||||
return;
|
||||
}
|
||||
WindowInfo* info = it->second.get();
|
||||
|
||||
if (!events->empty() || !info->mPendingWrNotifierEvents.empty()) {
|
||||
PostWrNotifierEvents(aWindowId, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderThread::WrNotifierEvent_HandleWakeUp(wr::WindowId aWindowId,
|
||||
bool aCompositeNeeded) {
|
||||
MOZ_ASSERT(IsInRenderThread());
|
||||
|
||||
bool isTrackedFrame = false;
|
||||
HandleFrameOneDoc(aWindowId, aCompositeNeeded, isTrackedFrame, Nothing());
|
||||
}
|
||||
|
||||
void RenderThread::WrNotifierEvent_HandleNewFrameReady(
|
||||
wr::WindowId aWindowId, bool aCompositeNeeded, FramePublishId aPublishId) {
|
||||
MOZ_ASSERT(IsInRenderThread());
|
||||
|
||||
bool isTrackedFrame = true;
|
||||
HandleFrameOneDoc(aWindowId, aCompositeNeeded, isTrackedFrame,
|
||||
Some(aPublishId));
|
||||
}
|
||||
|
||||
void RenderThread::WrNotifierEvent_HandleExternalEvent(
|
||||
wr::WindowId aWindowId, UniquePtr<RendererEvent> aRendererEvent) {
|
||||
MOZ_ASSERT(IsInRenderThread());
|
||||
|
||||
RunEvent(aWindowId, std::move(aRendererEvent));
|
||||
}
|
||||
|
||||
void RenderThread::BeginRecordingForWindow(wr::WindowId aWindowId,
|
||||
const TimeStamp& aRecordingStart,
|
||||
wr::PipelineId aRootPipelineId) {
|
||||
|
@ -388,22 +564,16 @@ Maybe<layers::FrameRecording> RenderThread::EndRecordingForWindow(
|
|||
return renderer->EndRecording();
|
||||
}
|
||||
|
||||
void RenderThread::PostHandleFrameOneDoc(wr::WindowId aWindowId, bool aRender,
|
||||
bool aTrackedFrame) {
|
||||
PostRunnable(NewRunnableMethod<wr::WindowId, bool, bool>(
|
||||
"wr::RenderThread::HandleFrameOneDoc", this,
|
||||
&RenderThread::HandleFrameOneDoc, aWindowId, aRender, aTrackedFrame));
|
||||
}
|
||||
|
||||
void RenderThread::HandleFrameOneDoc(wr::WindowId aWindowId, bool aRender,
|
||||
bool aTrackedFrame) {
|
||||
bool aTrackedFrame,
|
||||
Maybe<FramePublishId> aPublishId) {
|
||||
MOZ_ASSERT(IsInRenderThread());
|
||||
|
||||
if (mHasShutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
HandleFrameOneDocInner(aWindowId, aRender, aTrackedFrame);
|
||||
HandleFrameOneDocInner(aWindowId, aRender, aTrackedFrame, aPublishId);
|
||||
|
||||
if (aTrackedFrame) {
|
||||
DecPendingFrameCount(aWindowId);
|
||||
|
@ -411,7 +581,8 @@ void RenderThread::HandleFrameOneDoc(wr::WindowId aWindowId, bool aRender,
|
|||
}
|
||||
|
||||
void RenderThread::HandleFrameOneDocInner(wr::WindowId aWindowId, bool aRender,
|
||||
bool aTrackedFrame) {
|
||||
bool aTrackedFrame,
|
||||
Maybe<FramePublishId> aPublishId) {
|
||||
if (IsDestroyed(aWindowId)) {
|
||||
return;
|
||||
}
|
||||
|
@ -451,6 +622,10 @@ void RenderThread::HandleFrameOneDocInner(wr::WindowId aWindowId, bool aRender,
|
|||
// RenderTextureHost::Lock().
|
||||
HandleRenderTextureOps();
|
||||
|
||||
if (aPublishId.isSome()) {
|
||||
SetFramePublishId(aWindowId, aPublishId.ref());
|
||||
}
|
||||
|
||||
UpdateAndRender(aWindowId, frame.mStartId, frame.mStartTime, render,
|
||||
/* aReadbackSize */ Nothing(),
|
||||
/* aReadbackFormat */ Nothing(),
|
||||
|
@ -556,6 +731,20 @@ static void NotifyDidStartRender(layers::CompositorBridgeParent* aBridge) {
|
|||
}
|
||||
}
|
||||
|
||||
void RenderThread::SetFramePublishId(wr::WindowId aWindowId,
|
||||
FramePublishId aPublishId) {
|
||||
MOZ_ASSERT(IsInRenderThread());
|
||||
|
||||
auto it = mRenderers.find(aWindowId);
|
||||
MOZ_ASSERT(it != mRenderers.end());
|
||||
if (it == mRenderers.end()) {
|
||||
return;
|
||||
}
|
||||
auto& renderer = it->second;
|
||||
|
||||
renderer->SetFramePublishId(aPublishId);
|
||||
}
|
||||
|
||||
void RenderThread::UpdateAndRender(
|
||||
wr::WindowId aWindowId, const VsyncId& aStartId,
|
||||
const TimeStamp& aStartTime, bool aRender,
|
||||
|
@ -1406,9 +1595,8 @@ void wr_notifier_wake_up(mozilla::wr::WrWindowId aWindowId,
|
|||
bool aCompositeNeeded) {
|
||||
// wake_up is used for things like propagating debug options or memory
|
||||
// pressure events, so we are not tracking pending frame counts.
|
||||
bool isTrackedFrame = false;
|
||||
mozilla::wr::RenderThread::Get()->PostHandleFrameOneDoc(
|
||||
aWindowId, aCompositeNeeded, isTrackedFrame);
|
||||
mozilla::wr::RenderThread::Get()->WrNotifierEvent_WakeUp(aWindowId,
|
||||
aCompositeNeeded);
|
||||
}
|
||||
|
||||
void wr_notifier_new_frame_ready(mozilla::wr::WrWindowId aWindowId,
|
||||
|
@ -1417,17 +1605,14 @@ void wr_notifier_new_frame_ready(mozilla::wr::WrWindowId aWindowId,
|
|||
auto* renderThread = mozilla::wr::RenderThread::Get();
|
||||
renderThread->DecPendingFrameBuildCount(aWindowId);
|
||||
|
||||
bool isTrackedFrame = true;
|
||||
renderThread->PostHandleFrameOneDoc(aWindowId, aCompositeNeeded,
|
||||
isTrackedFrame);
|
||||
renderThread->WrNotifierEvent_NewFrameReady(aWindowId, aCompositeNeeded,
|
||||
aPublishId);
|
||||
}
|
||||
|
||||
void wr_notifier_external_event(mozilla::wr::WrWindowId aWindowId,
|
||||
size_t aRawEvent) {
|
||||
mozilla::UniquePtr<mozilla::wr::RendererEvent> evt(
|
||||
reinterpret_cast<mozilla::wr::RendererEvent*>(aRawEvent));
|
||||
mozilla::wr::RenderThread::Get()->PostEvent(mozilla::wr::WindowId(aWindowId),
|
||||
std::move(evt));
|
||||
mozilla::wr::RenderThread::Get()->WrNotifierEvent_ExternalEvent(
|
||||
mozilla::wr::WindowId(aWindowId), aRawEvent);
|
||||
}
|
||||
|
||||
static void NotifyScheduleRender(mozilla::wr::WrWindowId aWindowId,
|
||||
|
|
|
@ -165,14 +165,6 @@ class RenderThread final {
|
|||
/// Can only be called from the render thread.
|
||||
RendererOGL* GetRenderer(wr::WindowId aWindowId);
|
||||
|
||||
// RenderNotifier implementation
|
||||
|
||||
/// Forwarded to the render thread. Will trigger a render for
|
||||
/// the current pending frame once one call per document in that pending
|
||||
/// frame has been received.
|
||||
void PostHandleFrameOneDoc(wr::WindowId aWindowId, bool aRender,
|
||||
bool aTrackedFrame);
|
||||
|
||||
/// Automatically forwarded to the render thread.
|
||||
void SetClearColor(wr::WindowId aWindowId, wr::ColorF aColor);
|
||||
|
||||
|
@ -186,6 +178,9 @@ class RenderThread final {
|
|||
/// Post RendererEvent to the render thread.
|
||||
void PostEvent(wr::WindowId aWindowId, UniquePtr<RendererEvent> aEvent);
|
||||
|
||||
/// Can only be called from the render thread.
|
||||
void SetFramePublishId(wr::WindowId aWindowId, FramePublishId aPublishId);
|
||||
|
||||
/// Can only be called from the render thread.
|
||||
void UpdateAndRender(wr::WindowId aWindowId, const VsyncId& aStartId,
|
||||
const TimeStamp& aStartTime, bool aRender,
|
||||
|
@ -240,6 +235,13 @@ class RenderThread final {
|
|||
void DecPendingFrameBuildCount(wr::WindowId aWindowId);
|
||||
void DecPendingFrameCount(wr::WindowId aWindowId);
|
||||
|
||||
// RenderNotifier implementation
|
||||
void WrNotifierEvent_WakeUp(WrWindowId aWindowId, bool aCompositeNeeded);
|
||||
void WrNotifierEvent_NewFrameReady(WrWindowId aWindowId,
|
||||
bool aCompositeNeeded,
|
||||
FramePublishId aPublishId);
|
||||
void WrNotifierEvent_ExternalEvent(WrWindowId aWindowId, size_t aRawEvent);
|
||||
|
||||
/// Can be called from any thread.
|
||||
WebRenderThreadPool& ThreadPool() { return mThreadPool; }
|
||||
|
||||
|
@ -306,17 +308,85 @@ class RenderThread final {
|
|||
NotifyForUse,
|
||||
NotifyNotUsed,
|
||||
};
|
||||
class WrNotifierEvent {
|
||||
public:
|
||||
enum class Tag {
|
||||
WakeUp,
|
||||
NewFrameReady,
|
||||
ExternalEvent,
|
||||
};
|
||||
const Tag mTag;
|
||||
|
||||
private:
|
||||
WrNotifierEvent(const Tag aTag, const bool aCompositeNeeded)
|
||||
: mTag(aTag), mCompositeNeeded(aCompositeNeeded) {
|
||||
MOZ_ASSERT(mTag == Tag::WakeUp);
|
||||
}
|
||||
WrNotifierEvent(const Tag aTag, bool aCompositeNeeded,
|
||||
FramePublishId aPublishId)
|
||||
: mTag(aTag),
|
||||
mCompositeNeeded(aCompositeNeeded),
|
||||
mPublishId(aPublishId) {
|
||||
MOZ_ASSERT(mTag == Tag::NewFrameReady);
|
||||
}
|
||||
WrNotifierEvent(const Tag aTag, UniquePtr<RendererEvent> aRendererEvent)
|
||||
: mTag(aTag), mRendererEvent(std::move(aRendererEvent)) {
|
||||
MOZ_ASSERT(mTag == Tag::ExternalEvent);
|
||||
}
|
||||
|
||||
const bool mCompositeNeeded = false;
|
||||
UniquePtr<RendererEvent> mRendererEvent;
|
||||
const FramePublishId mPublishId = FramePublishId::INVALID;
|
||||
|
||||
public:
|
||||
static WrNotifierEvent WakeUp(const bool aCompositeNeeded) {
|
||||
return WrNotifierEvent(Tag::WakeUp, aCompositeNeeded);
|
||||
}
|
||||
|
||||
static WrNotifierEvent NewFrameReady(const bool aCompositeNeeded,
|
||||
const FramePublishId aPublishId) {
|
||||
return WrNotifierEvent(Tag::NewFrameReady, aCompositeNeeded, aPublishId);
|
||||
}
|
||||
|
||||
static WrNotifierEvent ExternalEvent(
|
||||
UniquePtr<RendererEvent> aRendererEvent) {
|
||||
return WrNotifierEvent(Tag::ExternalEvent, std::move(aRendererEvent));
|
||||
}
|
||||
|
||||
bool CompositeNeeded() {
|
||||
if (mTag == Tag::WakeUp || mTag == Tag::NewFrameReady) {
|
||||
return mCompositeNeeded;
|
||||
}
|
||||
MOZ_ASSERT_UNREACHABLE("unexpected to be called");
|
||||
return false;
|
||||
}
|
||||
FramePublishId PublishId() {
|
||||
if (mTag == Tag::NewFrameReady) {
|
||||
return mPublishId;
|
||||
}
|
||||
MOZ_ASSERT_UNREACHABLE("unexpected to be called");
|
||||
return FramePublishId::INVALID;
|
||||
}
|
||||
UniquePtr<RendererEvent> ExternalEvent() {
|
||||
if (mTag == Tag::ExternalEvent) {
|
||||
return std::move(mRendererEvent);
|
||||
}
|
||||
MOZ_ASSERT_UNREACHABLE("unexpected to be called");
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
explicit RenderThread(RefPtr<nsIThread> aThread);
|
||||
|
||||
void HandleFrameOneDocInner(wr::WindowId aWindowId, bool aRender,
|
||||
bool aTrackedFrame);
|
||||
bool aTrackedFrame,
|
||||
Maybe<FramePublishId> aPublishId);
|
||||
|
||||
void DeferredRenderTextureHostDestroy();
|
||||
void ShutDownTask();
|
||||
void InitDeviceTask();
|
||||
void HandleFrameOneDoc(wr::WindowId aWindowId, bool aRender,
|
||||
bool aTrackedFrame);
|
||||
bool aTrackedFrame, Maybe<FramePublishId> aPublishId);
|
||||
void RunEvent(wr::WindowId aWindowId, UniquePtr<RendererEvent> aEvent);
|
||||
void PostRunnable(already_AddRefed<nsIRunnable> aRunnable);
|
||||
|
||||
|
@ -330,6 +400,19 @@ class RenderThread final {
|
|||
|
||||
void DestroyExternalImages(const std::vector<wr::ExternalImageId>&& aIds);
|
||||
|
||||
struct WindowInfo;
|
||||
|
||||
void PostWrNotifierEvents(WrWindowId aWindowId);
|
||||
void PostWrNotifierEvents(WrWindowId aWindowId, WindowInfo* aInfo);
|
||||
void HandleWrNotifierEvents(WrWindowId aWindowId);
|
||||
void WrNotifierEvent_HandleWakeUp(wr::WindowId aWindowId,
|
||||
bool aCompositeNeeded);
|
||||
void WrNotifierEvent_HandleNewFrameReady(wr::WindowId aWindowId,
|
||||
bool aCompositeNeeded,
|
||||
FramePublishId aPublishId);
|
||||
void WrNotifierEvent_HandleExternalEvent(
|
||||
wr::WindowId aWindowId, UniquePtr<RendererEvent> aRendererEvent);
|
||||
|
||||
~RenderThread();
|
||||
|
||||
RefPtr<nsIThread> const mThread;
|
||||
|
@ -361,10 +444,15 @@ class RenderThread final {
|
|||
std::queue<PendingFrameInfo> mPendingFrames;
|
||||
uint8_t mPendingFrameBuild = 0;
|
||||
bool mIsDestroyed = false;
|
||||
RefPtr<nsIRunnable> mWrNotifierEventsRunnable;
|
||||
std::queue<WrNotifierEvent> mPendingWrNotifierEvents;
|
||||
};
|
||||
|
||||
DataMutex<std::unordered_map<uint64_t, UniquePtr<WindowInfo>>> mWindowInfos;
|
||||
|
||||
std::unordered_map<uint64_t, UniquePtr<std::queue<WrNotifierEvent>>>
|
||||
mWrNotifierEventsQueues;
|
||||
|
||||
struct ExternalImageIdHashFn {
|
||||
std::size_t operator()(const wr::ExternalImageId& aId) const {
|
||||
return HashGeneric(wr::AsUint64(aId));
|
||||
|
|
|
@ -130,6 +130,10 @@ wr::WrExternalImageHandler RendererOGL::GetExternalImageHandler() {
|
|||
};
|
||||
}
|
||||
|
||||
void RendererOGL::SetFramePublishId(FramePublishId aPublishId) {
|
||||
wr_renderer_set_target_frame_publish_id(mRenderer, aPublishId);
|
||||
}
|
||||
|
||||
void RendererOGL::Update() {
|
||||
mCompositor->Update();
|
||||
if (mCompositor->MakeCurrent()) {
|
||||
|
|
|
@ -55,6 +55,8 @@ class RendererOGL {
|
|||
public:
|
||||
wr::WrExternalImageHandler GetExternalImageHandler();
|
||||
|
||||
void SetFramePublishId(FramePublishId aPublishId);
|
||||
|
||||
/// This can be called on the render thread only.
|
||||
void Update();
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче