ryujinx/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs

363 lines
13 KiB
C#
Raw Normal View History

Add a Multithreading layer for the GAL, multi-thread shader compilation at runtime (#2501) * Initial Implementation About as fast as nvidia GL multithreading, can be improved with faster command queuing. * Struct based command list Speeds up a bit. Still a lot of time lost to resource copy. * Do shader init while the render thread is active. * Introduce circular span pool V1 Ideally should be able to use structs instead of references for storing these spans on commands. Will try that next. * Refactor SpanRef some more Use a struct to represent SpanRef, rather than a reference. * Flush buffers on background thread * Use a span for UpdateRenderScale. Much faster than copying the array. * Calculate command size using reflection * WIP parallel shaders * Some minor optimisation * Only 2 max refs per command now. The command with 3 refs is gone. :relieved: * Don't cast on the GPU side * Remove redundant casts, force sync on window present * Fix Shader Cache * Fix host shader save. * Fixup to work with new renderer stuff * Make command Run static, use array of delegates as lookup Profile says this takes less time than the previous way. * Bring up to date * Add settings toggle. Fix Muiltithreading Off mode. * Fix warning. * Release tracking lock for flushes * Fix Conditional Render fast path with threaded gal * Make handle iteration safe when releasing the lock This is mostly temporary. * Attempt to set backend threading on driver Only really works on nvidia before launching a game. * Fix race condition with BufferModifiedRangeList, exceptions in tracking actions * Update buffer set commands * Some cleanup * Only use stutter workaround when using opengl renderer non-threaded * Add host-conditional reservation of counter events There has always been the possibility that conditional rendering could use a query object just as it is disposed by the counter queue. This change makes it so that when the host decides to use host conditional rendering, the query object is reserved so that it cannot be deleted. Counter events can optionally start reserved, as the threaded implementation can reserve them before the backend creates them, and there would otherwise be a short amount of time where the counter queue could dispose the event before a call to reserve it could be made. * Address Feedback * Make counter flush tracked again. Hopefully does not cause any issues this time. * Wait for FlushTo on the main queue thread. Currently assumes only one thread will want to FlushTo (in this case, the GPU thread) * Add SDL2 headless integration * Add HLE macro commands. Co-authored-by: Mary <mary@mary.zone>
2021-08-26 22:31:29 +00:00
using Ryujinx.Graphics.GAL.Multithreading.Commands;
using Ryujinx.Graphics.GAL.Multithreading.Model;
using Ryujinx.Graphics.GAL.Multithreading.Resources;
using Ryujinx.Graphics.Shader;
using System;
using System.Linq;
namespace Ryujinx.Graphics.GAL.Multithreading
{
public class ThreadedPipeline : IPipeline
{
private ThreadedRenderer _renderer;
private IPipeline _impl;
public ThreadedPipeline(ThreadedRenderer renderer, IPipeline impl)
{
_renderer = renderer;
_impl = impl;
}
private TableRef<T> Ref<T>(T reference)
{
return new TableRef<T>(_renderer, reference);
}
public void Barrier()
{
_renderer.New<BarrierCommand>();
_renderer.QueueCommand();
}
public void BeginTransformFeedback(PrimitiveTopology topology)
{
_renderer.New<BeginTransformFeedbackCommand>().Set(topology);
_renderer.QueueCommand();
}
public void ClearBuffer(BufferHandle destination, int offset, int size, uint value)
{
_renderer.New<ClearBufferCommand>().Set(destination, offset, size, value);
_renderer.QueueCommand();
}
public void ClearRenderTargetColor(int index, uint componentMask, ColorF color)
{
_renderer.New<ClearRenderTargetColorCommand>().Set(index, componentMask, color);
_renderer.QueueCommand();
}
public void ClearRenderTargetDepthStencil(float depthValue, bool depthMask, int stencilValue, int stencilMask)
{
_renderer.New<ClearRenderTargetDepthStencilCommand>().Set(depthValue, depthMask, stencilValue, stencilMask);
_renderer.QueueCommand();
}
public void CommandBufferBarrier()
{
_renderer.New<CommandBufferBarrierCommand>();
_renderer.QueueCommand();
}
public void CopyBuffer(BufferHandle source, BufferHandle destination, int srcOffset, int dstOffset, int size)
{
_renderer.New<CopyBufferCommand>().Set(source, destination, srcOffset, dstOffset, size);
_renderer.QueueCommand();
}
public void DispatchCompute(int groupsX, int groupsY, int groupsZ)
{
_renderer.New<DispatchComputeCommand>().Set(groupsX, groupsY, groupsZ);
_renderer.QueueCommand();
}
public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance)
{
_renderer.New<DrawCommand>().Set(vertexCount, instanceCount, firstVertex, firstInstance);
_renderer.QueueCommand();
}
public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance)
{
_renderer.New<DrawIndexedCommand>().Set(indexCount, instanceCount, firstIndex, firstVertex, firstInstance);
_renderer.QueueCommand();
}
public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion)
{
_renderer.New<DrawTextureCommand>().Set(Ref(texture), Ref(sampler), srcRegion, dstRegion);
_renderer.QueueCommand();
}
Add a Multithreading layer for the GAL, multi-thread shader compilation at runtime (#2501) * Initial Implementation About as fast as nvidia GL multithreading, can be improved with faster command queuing. * Struct based command list Speeds up a bit. Still a lot of time lost to resource copy. * Do shader init while the render thread is active. * Introduce circular span pool V1 Ideally should be able to use structs instead of references for storing these spans on commands. Will try that next. * Refactor SpanRef some more Use a struct to represent SpanRef, rather than a reference. * Flush buffers on background thread * Use a span for UpdateRenderScale. Much faster than copying the array. * Calculate command size using reflection * WIP parallel shaders * Some minor optimisation * Only 2 max refs per command now. The command with 3 refs is gone. :relieved: * Don't cast on the GPU side * Remove redundant casts, force sync on window present * Fix Shader Cache * Fix host shader save. * Fixup to work with new renderer stuff * Make command Run static, use array of delegates as lookup Profile says this takes less time than the previous way. * Bring up to date * Add settings toggle. Fix Muiltithreading Off mode. * Fix warning. * Release tracking lock for flushes * Fix Conditional Render fast path with threaded gal * Make handle iteration safe when releasing the lock This is mostly temporary. * Attempt to set backend threading on driver Only really works on nvidia before launching a game. * Fix race condition with BufferModifiedRangeList, exceptions in tracking actions * Update buffer set commands * Some cleanup * Only use stutter workaround when using opengl renderer non-threaded * Add host-conditional reservation of counter events There has always been the possibility that conditional rendering could use a query object just as it is disposed by the counter queue. This change makes it so that when the host decides to use host conditional rendering, the query object is reserved so that it cannot be deleted. Counter events can optionally start reserved, as the threaded implementation can reserve them before the backend creates them, and there would otherwise be a short amount of time where the counter queue could dispose the event before a call to reserve it could be made. * Address Feedback * Make counter flush tracked again. Hopefully does not cause any issues this time. * Wait for FlushTo on the main queue thread. Currently assumes only one thread will want to FlushTo (in this case, the GPU thread) * Add SDL2 headless integration * Add HLE macro commands. Co-authored-by: Mary <mary@mary.zone>
2021-08-26 22:31:29 +00:00
public void EndHostConditionalRendering()
{
_renderer.New<EndHostConditionalRenderingCommand>();
_renderer.QueueCommand();
}
public void EndTransformFeedback()
{
_renderer.New<EndTransformFeedbackCommand>();
_renderer.QueueCommand();
}
public void MultiDrawIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride)
{
_renderer.New<MultiDrawIndirectCountCommand>().Set(indirectBuffer, parameterBuffer, maxDrawCount, stride);
_renderer.QueueCommand();
}
public void MultiDrawIndexedIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride)
{
_renderer.New<MultiDrawIndexedIndirectCountCommand>().Set(indirectBuffer, parameterBuffer, maxDrawCount, stride);
_renderer.QueueCommand();
}
public void SetAlphaTest(bool enable, float reference, CompareOp op)
{
_renderer.New<SetAlphaTestCommand>().Set(enable, reference, op);
_renderer.QueueCommand();
}
public void SetBlendState(int index, BlendDescriptor blend)
{
_renderer.New<SetBlendStateCommand>().Set(index, blend);
_renderer.QueueCommand();
}
public void SetDepthBias(PolygonModeMask enables, float factor, float units, float clamp)
{
_renderer.New<SetDepthBiasCommand>().Set(enables, factor, units, clamp);
_renderer.QueueCommand();
}
public void SetDepthClamp(bool clamp)
{
_renderer.New<SetDepthClampCommand>().Set(clamp);
_renderer.QueueCommand();
}
public void SetDepthMode(DepthMode mode)
{
_renderer.New<SetDepthModeCommand>().Set(mode);
_renderer.QueueCommand();
}
public void SetDepthTest(DepthTestDescriptor depthTest)
{
_renderer.New<SetDepthTestCommand>().Set(depthTest);
_renderer.QueueCommand();
}
public void SetFaceCulling(bool enable, Face face)
{
_renderer.New<SetFaceCullingCommand>().Set(enable, face);
_renderer.QueueCommand();
}
public void SetFrontFace(FrontFace frontFace)
{
_renderer.New<SetFrontFaceCommand>().Set(frontFace);
_renderer.QueueCommand();
}
public void SetImage(int binding, ITexture texture, Format imageFormat)
{
_renderer.New<SetImageCommand>().Set(binding, Ref(texture), imageFormat);
_renderer.QueueCommand();
}
public void SetIndexBuffer(BufferRange buffer, IndexType type)
{
_renderer.New<SetIndexBufferCommand>().Set(buffer, type);
_renderer.QueueCommand();
}
public void SetLineParameters(float width, bool smooth)
{
_renderer.New<SetLineParametersCommand>().Set(width, smooth);
_renderer.QueueCommand();
}
public void SetLogicOpState(bool enable, LogicalOp op)
{
_renderer.New<SetLogicOpStateCommand>().Set(enable, op);
_renderer.QueueCommand();
}
public void SetPatchParameters(int vertices, ReadOnlySpan<float> defaultOuterLevel, ReadOnlySpan<float> defaultInnerLevel)
{
_renderer.New<SetPatchParametersCommand>().Set(vertices, defaultOuterLevel, defaultInnerLevel);
_renderer.QueueCommand();
}
Add a Multithreading layer for the GAL, multi-thread shader compilation at runtime (#2501) * Initial Implementation About as fast as nvidia GL multithreading, can be improved with faster command queuing. * Struct based command list Speeds up a bit. Still a lot of time lost to resource copy. * Do shader init while the render thread is active. * Introduce circular span pool V1 Ideally should be able to use structs instead of references for storing these spans on commands. Will try that next. * Refactor SpanRef some more Use a struct to represent SpanRef, rather than a reference. * Flush buffers on background thread * Use a span for UpdateRenderScale. Much faster than copying the array. * Calculate command size using reflection * WIP parallel shaders * Some minor optimisation * Only 2 max refs per command now. The command with 3 refs is gone. :relieved: * Don't cast on the GPU side * Remove redundant casts, force sync on window present * Fix Shader Cache * Fix host shader save. * Fixup to work with new renderer stuff * Make command Run static, use array of delegates as lookup Profile says this takes less time than the previous way. * Bring up to date * Add settings toggle. Fix Muiltithreading Off mode. * Fix warning. * Release tracking lock for flushes * Fix Conditional Render fast path with threaded gal * Make handle iteration safe when releasing the lock This is mostly temporary. * Attempt to set backend threading on driver Only really works on nvidia before launching a game. * Fix race condition with BufferModifiedRangeList, exceptions in tracking actions * Update buffer set commands * Some cleanup * Only use stutter workaround when using opengl renderer non-threaded * Add host-conditional reservation of counter events There has always been the possibility that conditional rendering could use a query object just as it is disposed by the counter queue. This change makes it so that when the host decides to use host conditional rendering, the query object is reserved so that it cannot be deleted. Counter events can optionally start reserved, as the threaded implementation can reserve them before the backend creates them, and there would otherwise be a short amount of time where the counter queue could dispose the event before a call to reserve it could be made. * Address Feedback * Make counter flush tracked again. Hopefully does not cause any issues this time. * Wait for FlushTo on the main queue thread. Currently assumes only one thread will want to FlushTo (in this case, the GPU thread) * Add SDL2 headless integration * Add HLE macro commands. Co-authored-by: Mary <mary@mary.zone>
2021-08-26 22:31:29 +00:00
public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin)
{
_renderer.New<SetPointParametersCommand>().Set(size, isProgramPointSize, enablePointSprite, origin);
_renderer.QueueCommand();
}
public void SetPolygonMode(PolygonMode frontMode, PolygonMode backMode)
{
_renderer.New<SetPolygonModeCommand>().Set(frontMode, backMode);
_renderer.QueueCommand();
}
Add a Multithreading layer for the GAL, multi-thread shader compilation at runtime (#2501) * Initial Implementation About as fast as nvidia GL multithreading, can be improved with faster command queuing. * Struct based command list Speeds up a bit. Still a lot of time lost to resource copy. * Do shader init while the render thread is active. * Introduce circular span pool V1 Ideally should be able to use structs instead of references for storing these spans on commands. Will try that next. * Refactor SpanRef some more Use a struct to represent SpanRef, rather than a reference. * Flush buffers on background thread * Use a span for UpdateRenderScale. Much faster than copying the array. * Calculate command size using reflection * WIP parallel shaders * Some minor optimisation * Only 2 max refs per command now. The command with 3 refs is gone. :relieved: * Don't cast on the GPU side * Remove redundant casts, force sync on window present * Fix Shader Cache * Fix host shader save. * Fixup to work with new renderer stuff * Make command Run static, use array of delegates as lookup Profile says this takes less time than the previous way. * Bring up to date * Add settings toggle. Fix Muiltithreading Off mode. * Fix warning. * Release tracking lock for flushes * Fix Conditional Render fast path with threaded gal * Make handle iteration safe when releasing the lock This is mostly temporary. * Attempt to set backend threading on driver Only really works on nvidia before launching a game. * Fix race condition with BufferModifiedRangeList, exceptions in tracking actions * Update buffer set commands * Some cleanup * Only use stutter workaround when using opengl renderer non-threaded * Add host-conditional reservation of counter events There has always been the possibility that conditional rendering could use a query object just as it is disposed by the counter queue. This change makes it so that when the host decides to use host conditional rendering, the query object is reserved so that it cannot be deleted. Counter events can optionally start reserved, as the threaded implementation can reserve them before the backend creates them, and there would otherwise be a short amount of time where the counter queue could dispose the event before a call to reserve it could be made. * Address Feedback * Make counter flush tracked again. Hopefully does not cause any issues this time. * Wait for FlushTo on the main queue thread. Currently assumes only one thread will want to FlushTo (in this case, the GPU thread) * Add SDL2 headless integration * Add HLE macro commands. Co-authored-by: Mary <mary@mary.zone>
2021-08-26 22:31:29 +00:00
public void SetPrimitiveRestart(bool enable, int index)
{
_renderer.New<SetPrimitiveRestartCommand>().Set(enable, index);
_renderer.QueueCommand();
}
public void SetPrimitiveTopology(PrimitiveTopology topology)
{
_renderer.New<SetPrimitiveTopologyCommand>().Set(topology);
_renderer.QueueCommand();
}
public void SetProgram(IProgram program)
{
_renderer.New<SetProgramCommand>().Set(Ref(program));
_renderer.QueueCommand();
}
public void SetRasterizerDiscard(bool discard)
{
_renderer.New<SetRasterizerDiscardCommand>().Set(discard);
_renderer.QueueCommand();
}
public void SetRenderTargetColorMasks(ReadOnlySpan<uint> componentMask)
{
_renderer.New<SetRenderTargetColorMasksCommand>().Set(_renderer.CopySpan(componentMask));
_renderer.QueueCommand();
}
public void SetRenderTargets(ITexture[] colors, ITexture depthStencil)
{
_renderer.New<SetRenderTargetsCommand>().Set(Ref(colors.ToArray()), Ref(depthStencil));
_renderer.QueueCommand();
}
public void SetRenderTargetScale(float scale)
{
_renderer.New<SetRenderTargetScaleCommand>().Set(scale);
_renderer.QueueCommand();
}
public void SetSampler(int binding, ISampler sampler)
{
_renderer.New<SetSamplerCommand>().Set(binding, Ref(sampler));
_renderer.QueueCommand();
}
public void SetScissor(int index, bool enable, int x, int y, int width, int height)
{
_renderer.New<SetScissorCommand>().Set(index, enable, x, y, width, height);
_renderer.QueueCommand();
}
public void SetStencilTest(StencilTestDescriptor stencilTest)
{
_renderer.New<SetStencilTestCommand>().Set(stencilTest);
_renderer.QueueCommand();
}
public void SetStorageBuffers(int first, ReadOnlySpan<BufferRange> buffers)
{
_renderer.New<SetStorageBuffersCommand>().Set(first, _renderer.CopySpan(buffers));
_renderer.QueueCommand();
}
public void SetTexture(int binding, ITexture texture)
{
_renderer.New<SetTextureCommand>().Set(binding, Ref(texture));
_renderer.QueueCommand();
}
public void SetTransformFeedbackBuffers(ReadOnlySpan<BufferRange> buffers)
{
_renderer.New<SetTransformFeedbackBuffersCommand>().Set(_renderer.CopySpan(buffers));
_renderer.QueueCommand();
}
public void SetUniformBuffers(int first, ReadOnlySpan<BufferRange> buffers)
{
_renderer.New<SetUniformBuffersCommand>().Set(first, _renderer.CopySpan(buffers));
_renderer.QueueCommand();
}
public void SetUserClipDistance(int index, bool enableClip)
{
_renderer.New<SetUserClipDistanceCommand>().Set(index, enableClip);
_renderer.QueueCommand();
}
public void SetVertexAttribs(ReadOnlySpan<VertexAttribDescriptor> vertexAttribs)
{
_renderer.New<SetVertexAttribsCommand>().Set(_renderer.CopySpan(vertexAttribs));
_renderer.QueueCommand();
}
public void SetVertexBuffers(ReadOnlySpan<VertexBufferDescriptor> vertexBuffers)
{
_renderer.New<SetVertexBuffersCommand>().Set(_renderer.CopySpan(vertexBuffers));
_renderer.QueueCommand();
}
public void SetViewports(int first, ReadOnlySpan<Viewport> viewports)
{
_renderer.New<SetViewportsCommand>().Set(first, _renderer.CopySpan(viewports));
_renderer.QueueCommand();
}
public void TextureBarrier()
{
_renderer.New<TextureBarrierCommand>();
_renderer.QueueCommand();
}
public void TextureBarrierTiled()
{
_renderer.New<TextureBarrierTiledCommand>();
_renderer.QueueCommand();
}
public bool TryHostConditionalRendering(ICounterEvent value, ulong compare, bool isEqual)
{
var evt = value as ThreadedCounterEvent;
if (evt != null)
{
if (compare == 0 && evt.Type == CounterType.SamplesPassed && evt.ClearCounter)
{
if (!evt.ReserveForHostAccess())
{
return false;
}
_renderer.New<TryHostConditionalRenderingCommand>().Set(Ref(evt), compare, isEqual);
_renderer.QueueCommand();
return true;
}
}
_renderer.New<TryHostConditionalRenderingFlushCommand>().Set(Ref(evt), Ref<ThreadedCounterEvent>(null), isEqual);
_renderer.QueueCommand();
return false;
}
public bool TryHostConditionalRendering(ICounterEvent value, ICounterEvent compare, bool isEqual)
{
_renderer.New<TryHostConditionalRenderingFlushCommand>().Set(Ref(value as ThreadedCounterEvent), Ref(compare as ThreadedCounterEvent), isEqual);
_renderer.QueueCommand();
return false;
}
public void UpdateRenderScale(ReadOnlySpan<float> scales, int totalCount, int fragmentCount)
Add a Multithreading layer for the GAL, multi-thread shader compilation at runtime (#2501) * Initial Implementation About as fast as nvidia GL multithreading, can be improved with faster command queuing. * Struct based command list Speeds up a bit. Still a lot of time lost to resource copy. * Do shader init while the render thread is active. * Introduce circular span pool V1 Ideally should be able to use structs instead of references for storing these spans on commands. Will try that next. * Refactor SpanRef some more Use a struct to represent SpanRef, rather than a reference. * Flush buffers on background thread * Use a span for UpdateRenderScale. Much faster than copying the array. * Calculate command size using reflection * WIP parallel shaders * Some minor optimisation * Only 2 max refs per command now. The command with 3 refs is gone. :relieved: * Don't cast on the GPU side * Remove redundant casts, force sync on window present * Fix Shader Cache * Fix host shader save. * Fixup to work with new renderer stuff * Make command Run static, use array of delegates as lookup Profile says this takes less time than the previous way. * Bring up to date * Add settings toggle. Fix Muiltithreading Off mode. * Fix warning. * Release tracking lock for flushes * Fix Conditional Render fast path with threaded gal * Make handle iteration safe when releasing the lock This is mostly temporary. * Attempt to set backend threading on driver Only really works on nvidia before launching a game. * Fix race condition with BufferModifiedRangeList, exceptions in tracking actions * Update buffer set commands * Some cleanup * Only use stutter workaround when using opengl renderer non-threaded * Add host-conditional reservation of counter events There has always been the possibility that conditional rendering could use a query object just as it is disposed by the counter queue. This change makes it so that when the host decides to use host conditional rendering, the query object is reserved so that it cannot be deleted. Counter events can optionally start reserved, as the threaded implementation can reserve them before the backend creates them, and there would otherwise be a short amount of time where the counter queue could dispose the event before a call to reserve it could be made. * Address Feedback * Make counter flush tracked again. Hopefully does not cause any issues this time. * Wait for FlushTo on the main queue thread. Currently assumes only one thread will want to FlushTo (in this case, the GPU thread) * Add SDL2 headless integration * Add HLE macro commands. Co-authored-by: Mary <mary@mary.zone>
2021-08-26 22:31:29 +00:00
{
_renderer.New<UpdateRenderScaleCommand>().Set(_renderer.CopySpan(scales.Slice(0, totalCount)), totalCount, fragmentCount);
Add a Multithreading layer for the GAL, multi-thread shader compilation at runtime (#2501) * Initial Implementation About as fast as nvidia GL multithreading, can be improved with faster command queuing. * Struct based command list Speeds up a bit. Still a lot of time lost to resource copy. * Do shader init while the render thread is active. * Introduce circular span pool V1 Ideally should be able to use structs instead of references for storing these spans on commands. Will try that next. * Refactor SpanRef some more Use a struct to represent SpanRef, rather than a reference. * Flush buffers on background thread * Use a span for UpdateRenderScale. Much faster than copying the array. * Calculate command size using reflection * WIP parallel shaders * Some minor optimisation * Only 2 max refs per command now. The command with 3 refs is gone. :relieved: * Don't cast on the GPU side * Remove redundant casts, force sync on window present * Fix Shader Cache * Fix host shader save. * Fixup to work with new renderer stuff * Make command Run static, use array of delegates as lookup Profile says this takes less time than the previous way. * Bring up to date * Add settings toggle. Fix Muiltithreading Off mode. * Fix warning. * Release tracking lock for flushes * Fix Conditional Render fast path with threaded gal * Make handle iteration safe when releasing the lock This is mostly temporary. * Attempt to set backend threading on driver Only really works on nvidia before launching a game. * Fix race condition with BufferModifiedRangeList, exceptions in tracking actions * Update buffer set commands * Some cleanup * Only use stutter workaround when using opengl renderer non-threaded * Add host-conditional reservation of counter events There has always been the possibility that conditional rendering could use a query object just as it is disposed by the counter queue. This change makes it so that when the host decides to use host conditional rendering, the query object is reserved so that it cannot be deleted. Counter events can optionally start reserved, as the threaded implementation can reserve them before the backend creates them, and there would otherwise be a short amount of time where the counter queue could dispose the event before a call to reserve it could be made. * Address Feedback * Make counter flush tracked again. Hopefully does not cause any issues this time. * Wait for FlushTo on the main queue thread. Currently assumes only one thread will want to FlushTo (in this case, the GPU thread) * Add SDL2 headless integration * Add HLE macro commands. Co-authored-by: Mary <mary@mary.zone>
2021-08-26 22:31:29 +00:00
_renderer.QueueCommand();
}
}
}