| // Copyright 2016 The SwiftShader Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "Renderer.hpp" |
| |
| #include "Clipper.hpp" |
| #include "Primitive.hpp" |
| #include "Polygon.hpp" |
| #include "Device/SwiftConfig.hpp" |
| #include "Reactor/Reactor.hpp" |
| #include "Pipeline/Constants.hpp" |
| #include "System/MutexLock.hpp" |
| #include "System/CPUID.hpp" |
| #include "System/Memory.hpp" |
| #include "System/Resource.hpp" |
| #include "System/Half.hpp" |
| #include "System/Math.hpp" |
| #include "System/Timer.hpp" |
| #include "Vulkan/VkConfig.h" |
| #include "Vulkan/VkDebug.hpp" |
| #include "Vulkan/VkImageView.hpp" |
| #include "Vulkan/VkQueryPool.hpp" |
| #include "Pipeline/SpirvShader.hpp" |
| #include "Vertex.hpp" |
| |
| #undef max |
| |
| bool disableServer = true; |
| |
| #ifndef NDEBUG |
| unsigned int minPrimitives = 1; |
| unsigned int maxPrimitives = 1 << 21; |
| #endif |
| |
| namespace sw |
| { |
| extern bool booleanFaceRegister; |
| extern bool fullPixelPositionRegister; |
| |
| extern bool forceWindowed; |
| extern bool postBlendSRGB; |
| extern bool exactColorRounding; |
| extern TransparencyAntialiasing transparencyAntialiasing; |
| extern bool forceClearRegisters; |
| |
| extern bool precacheVertex; |
| extern bool precacheSetup; |
| extern bool precachePixel; |
| |
| static const int batchSize = 128; |
| AtomicInt threadCount(1); |
| AtomicInt Renderer::unitCount(1); |
| AtomicInt Renderer::clusterCount(1); |
| |
| TranscendentalPrecision logPrecision = ACCURATE; |
| TranscendentalPrecision expPrecision = ACCURATE; |
| TranscendentalPrecision rcpPrecision = ACCURATE; |
| TranscendentalPrecision rsqPrecision = ACCURATE; |
| bool perspectiveCorrection = true; |
| |
| static void setGlobalRenderingSettings(Conventions conventions, bool exactColorRounding) |
| { |
| static bool initialized = false; |
| |
| if(!initialized) |
| { |
| sw::booleanFaceRegister = conventions.booleanFaceRegister; |
| sw::fullPixelPositionRegister = conventions.fullPixelPositionRegister; |
| sw::exactColorRounding = exactColorRounding; |
| initialized = true; |
| } |
| } |
| |
| template<typename T> |
| inline bool setBatchIndices(unsigned int batch[128][3], VkPrimitiveTopology topology, T indices, unsigned int start, unsigned int triangleCount) |
| { |
| switch(topology) |
| { |
| case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: |
| { |
| auto index = start; |
| for(unsigned int i = 0; i < triangleCount; i++) |
| { |
| batch[i][0] = indices[index]; |
| batch[i][1] = indices[index]; |
| batch[i][2] = indices[index]; |
| |
| index += 1; |
| } |
| break; |
| } |
| case VK_PRIMITIVE_TOPOLOGY_LINE_LIST: |
| { |
| auto index = 2 * start; |
| for(unsigned int i = 0; i < triangleCount; i++) |
| { |
| batch[i][0] = indices[index + 0]; |
| batch[i][1] = indices[index + 1]; |
| batch[i][2] = indices[index + 1]; |
| |
| index += 2; |
| } |
| break; |
| } |
| case VK_PRIMITIVE_TOPOLOGY_LINE_STRIP: |
| { |
| auto index = start; |
| for(unsigned int i = 0; i < triangleCount; i++) |
| { |
| batch[i][0] = indices[index + 0]; |
| batch[i][1] = indices[index + 1]; |
| batch[i][2] = indices[index + 1]; |
| |
| index += 1; |
| } |
| break; |
| } |
| case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST: |
| { |
| auto index = 3 * start; |
| for(unsigned int i = 0; i < triangleCount; i++) |
| { |
| batch[i][0] = indices[index + 0]; |
| batch[i][1] = indices[index + 1]; |
| batch[i][2] = indices[index + 2]; |
| |
| index += 3; |
| } |
| break; |
| } |
| case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP: |
| { |
| auto index = start; |
| for(unsigned int i = 0; i < triangleCount; i++) |
| { |
| batch[i][0] = indices[index + 0]; |
| batch[i][1] = indices[index + ((start + i) & 1) + 1]; |
| batch[i][2] = indices[index + (~(start + i) & 1) + 1]; |
| |
| index += 1; |
| } |
| break; |
| } |
| case VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN: |
| { |
| auto index = start + 1; |
| for(unsigned int i = 0; i < triangleCount; i++) |
| { |
| batch[i][0] = indices[index + 0]; |
| batch[i][1] = indices[index + 1]; |
| batch[i][2] = indices[0]; |
| |
| index += 1; |
| } |
| break; |
| } |
| default: |
| ASSERT(false); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| struct Parameters |
| { |
| Renderer *renderer; |
| int threadIndex; |
| }; |
| |
| DrawCall::DrawCall() |
| { |
| queries = 0; |
| |
| references = -1; |
| |
| data = (DrawData*)allocate(sizeof(DrawData)); |
| data->constants = &constants; |
| } |
| |
| DrawCall::~DrawCall() |
| { |
| delete queries; |
| |
| deallocate(data); |
| } |
| |
| Renderer::Renderer(Context *context, Conventions conventions, bool exactColorRounding) : VertexProcessor(context), PixelProcessor(context), SetupProcessor(context), context(context), viewport() |
| { |
| setGlobalRenderingSettings(conventions, exactColorRounding); |
| |
| setRenderTarget(0, nullptr); |
| clipper = new Clipper; |
| blitter = new Blitter; |
| |
| #if PERF_HUD |
| resetTimers(); |
| #endif |
| |
| for(int i = 0; i < 16; i++) |
| { |
| vertexTask[i] = nullptr; |
| |
| worker[i] = nullptr; |
| resume[i] = nullptr; |
| suspend[i] = nullptr; |
| } |
| |
| threadsAwake = 0; |
| resumeApp = new Event(); |
| |
| currentDraw = 0; |
| nextDraw = 0; |
| |
| qHead = 0; |
| qSize = 0; |
| |
| for(int i = 0; i < 16; i++) |
| { |
| triangleBatch[i] = nullptr; |
| primitiveBatch[i] = nullptr; |
| } |
| |
| for(int draw = 0; draw < DRAW_COUNT; draw++) |
| { |
| drawCall[draw] = new DrawCall(); |
| drawList[draw] = drawCall[draw]; |
| } |
| |
| for(int unit = 0; unit < 16; unit++) |
| { |
| primitiveProgress[unit].init(); |
| } |
| |
| for(int cluster = 0; cluster < 16; cluster++) |
| { |
| pixelProgress[cluster].init(); |
| } |
| |
| clipFlags = 0; |
| |
| swiftConfig = new SwiftConfig(disableServer); |
| updateConfiguration(true); |
| |
| sync = new Resource(0); |
| } |
| |
| Renderer::~Renderer() |
| { |
| sync->lock(EXCLUSIVE); |
| sync->destruct(); |
| terminateThreads(); |
| sync->unlock(); |
| |
| delete clipper; |
| clipper = nullptr; |
| |
| delete blitter; |
| blitter = nullptr; |
| |
| delete resumeApp; |
| resumeApp = nullptr; |
| |
| for(int draw = 0; draw < DRAW_COUNT; draw++) |
| { |
| delete drawCall[draw]; |
| drawCall[draw] = nullptr; |
| } |
| |
| delete swiftConfig; |
| swiftConfig = nullptr; |
| } |
| |
| // This object has to be mem aligned |
| void* Renderer::operator new(size_t size) |
| { |
| ASSERT(size == sizeof(Renderer)); // This operator can't be called from a derived class |
| return sw::allocate(sizeof(Renderer), 16); |
| } |
| |
| void Renderer::operator delete(void * mem) |
| { |
| sw::deallocate(mem); |
| } |
| |
| void Renderer::draw(VkPrimitiveTopology topology, VkIndexType indexType, unsigned int count, int baseVertex, bool update) |
| { |
| #ifndef NDEBUG |
| if(count < minPrimitives || count > maxPrimitives) |
| { |
| return; |
| } |
| #endif |
| |
| context->topology = topology; |
| |
| updateConfiguration(); |
| |
| int ms = context->sampleCount; |
| context->multiSampleMask = context->sampleMask & ((unsigned)0xFFFFFFFF >> (32 - ms)); |
| |
| if(!context->multiSampleMask) |
| { |
| return; |
| } |
| |
| context->occlusionEnabled = false; |
| for(auto query : queries) |
| { |
| if(query->type == VK_QUERY_TYPE_OCCLUSION) |
| { |
| context->occlusionEnabled = true; |
| break; |
| } |
| } |
| |
| sync->lock(sw::PRIVATE); |
| |
| if(update) |
| { |
| vertexState = VertexProcessor::update(topology); |
| setupState = SetupProcessor::update(); |
| pixelState = PixelProcessor::update(); |
| |
| vertexRoutine = VertexProcessor::routine(vertexState); |
| setupRoutine = SetupProcessor::routine(setupState); |
| pixelRoutine = PixelProcessor::routine(pixelState); |
| } |
| |
| int batch = batchSize / ms; |
| |
| int (Renderer::*setupPrimitives)(int batch, int count); |
| |
| if(context->isDrawTriangle()) |
| { |
| setupPrimitives = &Renderer::setupTriangles; |
| } |
| else if(context->isDrawLine()) |
| { |
| setupPrimitives = &Renderer::setupLines; |
| } |
| else // Point draw |
| { |
| setupPrimitives = &Renderer::setupPoints; |
| } |
| |
| DrawCall *draw = nullptr; |
| |
| do |
| { |
| for(int i = 0; i < DRAW_COUNT; i++) |
| { |
| if(drawCall[i]->references == -1) |
| { |
| draw = drawCall[i]; |
| drawList[nextDraw & DRAW_COUNT_BITS] = draw; |
| |
| break; |
| } |
| } |
| |
| if(!draw) |
| { |
| resumeApp->wait(); |
| } |
| } |
| while(!draw); |
| |
| DrawData *data = draw->data; |
| |
| if(queries.size() != 0) |
| { |
| draw->queries = new std::list<vk::Query*>(); |
| for(auto &query : queries) |
| { |
| ++query->reference; // Atomic |
| draw->queries->push_back(query); |
| } |
| } |
| |
| draw->topology = topology; |
| draw->indexType = indexType; |
| draw->batchSize = batch; |
| |
| vertexRoutine->bind(); |
| setupRoutine->bind(); |
| pixelRoutine->bind(); |
| |
| draw->vertexRoutine = vertexRoutine; |
| draw->setupRoutine = setupRoutine; |
| draw->pixelRoutine = pixelRoutine; |
| draw->vertexPointer = (VertexProcessor::RoutinePointer)vertexRoutine->getEntry(); |
| draw->setupPointer = (SetupProcessor::RoutinePointer)setupRoutine->getEntry(); |
| draw->pixelPointer = (PixelProcessor::RoutinePointer)pixelRoutine->getEntry(); |
| draw->setupPrimitives = setupPrimitives; |
| draw->setupState = setupState; |
| |
| data->descriptorSets = context->descriptorSets; |
| data->descriptorDynamicOffsets = context->descriptorDynamicOffsets; |
| |
| for(int i = 0; i < MAX_VERTEX_INPUTS; i++) |
| { |
| data->input[i] = context->input[i].buffer; |
| data->stride[i] = context->input[i].vertexStride; |
| } |
| |
| data->indices = context->indexBuffer; |
| |
| if(context->vertexShader->hasBuiltinInput(spv::BuiltInInstanceIndex)) |
| { |
| data->instanceID = context->instanceID; |
| } |
| |
| data->baseVertex = baseVertex; |
| |
| if(pixelState.stencilActive) |
| { |
| data->stencil[0].set(context->frontStencil.reference, context->frontStencil.compareMask, context->frontStencil.writeMask); |
| data->stencil[1].set(context->backStencil.reference, context->backStencil.compareMask, context->backStencil.writeMask); |
| } |
| |
| data->lineWidth = context->lineWidth; |
| |
| data->factor = factor; |
| |
| if(pixelState.alphaToCoverage) |
| { |
| if(ms == 4) |
| { |
| data->a2c0 = replicate(0.2f); |
| data->a2c1 = replicate(0.4f); |
| data->a2c2 = replicate(0.6f); |
| data->a2c3 = replicate(0.8f); |
| } |
| else if(ms == 2) |
| { |
| data->a2c0 = replicate(0.25f); |
| data->a2c1 = replicate(0.75f); |
| } |
| else ASSERT(false); |
| } |
| |
| if(pixelState.occlusionEnabled) |
| { |
| for(int cluster = 0; cluster < clusterCount; cluster++) |
| { |
| data->occlusion[cluster] = 0; |
| } |
| } |
| |
| #if PERF_PROFILE |
| for(int cluster = 0; cluster < clusterCount; cluster++) |
| { |
| for(int i = 0; i < PERF_TIMERS; i++) |
| { |
| data->cycles[i][cluster] = 0; |
| } |
| } |
| #endif |
| |
| // Viewport |
| { |
| float W = 0.5f * viewport.width; |
| float H = 0.5f * viewport.height; |
| float X0 = viewport.x + W; |
| float Y0 = viewport.y + H; |
| float N = viewport.minDepth; |
| float F = viewport.maxDepth; |
| float Z = F - N; |
| |
| if(context->isDrawTriangle()) |
| { |
| N += context->depthBias; |
| } |
| |
| data->Wx16 = replicate(W * 16); |
| data->Hx16 = replicate(H * 16); |
| data->X0x16 = replicate(X0 * 16 - 8); |
| data->Y0x16 = replicate(Y0 * 16 - 8); |
| data->halfPixelX = replicate(0.5f / W); |
| data->halfPixelY = replicate(0.5f / H); |
| data->viewportHeight = abs(viewport.height); |
| data->slopeDepthBias = context->slopeDepthBias; |
| data->depthRange = Z; |
| data->depthNear = N; |
| } |
| |
| // Target |
| { |
| for(int index = 0; index < RENDERTARGETS; index++) |
| { |
| draw->renderTarget[index] = context->renderTarget[index]; |
| |
| if(draw->renderTarget[index]) |
| { |
| data->colorBuffer[index] = (unsigned int*)context->renderTarget[index]->getOffsetPointer({0, 0, 0}, VK_IMAGE_ASPECT_COLOR_BIT, 0); |
| data->colorPitchB[index] = context->renderTarget[index]->rowPitchBytes(VK_IMAGE_ASPECT_COLOR_BIT, 0); |
| data->colorSliceB[index] = context->renderTarget[index]->slicePitchBytes(VK_IMAGE_ASPECT_COLOR_BIT, 0); |
| } |
| } |
| |
| draw->depthBuffer = context->depthBuffer; |
| draw->stencilBuffer = context->stencilBuffer; |
| |
| if(draw->depthBuffer) |
| { |
| data->depthBuffer = (float*)context->depthBuffer->getOffsetPointer({0, 0, 0}, VK_IMAGE_ASPECT_DEPTH_BIT, 0); |
| data->depthPitchB = context->depthBuffer->rowPitchBytes(VK_IMAGE_ASPECT_DEPTH_BIT, 0); |
| data->depthSliceB = context->depthBuffer->slicePitchBytes(VK_IMAGE_ASPECT_DEPTH_BIT, 0); |
| } |
| |
| if(draw->stencilBuffer) |
| { |
| data->stencilBuffer = (unsigned char*)context->stencilBuffer->getOffsetPointer({0, 0, 0}, VK_IMAGE_ASPECT_STENCIL_BIT, 0); |
| data->stencilPitchB = context->stencilBuffer->rowPitchBytes(VK_IMAGE_ASPECT_STENCIL_BIT, 0); |
| data->stencilSliceB = context->stencilBuffer->slicePitchBytes(VK_IMAGE_ASPECT_STENCIL_BIT, 0); |
| } |
| } |
| |
| // Scissor |
| { |
| data->scissorX0 = scissor.offset.x; |
| data->scissorX1 = scissor.offset.x + scissor.extent.width; |
| data->scissorY0 = scissor.offset.y; |
| data->scissorY1 = scissor.offset.y + scissor.extent.height; |
| } |
| |
| // Push constants |
| { |
| data->pushConstants = context->pushConstants; |
| } |
| |
| draw->primitive = 0; |
| draw->count = count; |
| |
| draw->references = (count + batch - 1) / batch; |
| |
| schedulerMutex.lock(); |
| ++nextDraw; // Atomic |
| schedulerMutex.unlock(); |
| |
| #ifndef NDEBUG |
| if(threadCount == 1) // Use main thread for draw execution |
| { |
| threadsAwake = 1; |
| task[0].type = Task::RESUME; |
| |
| taskLoop(0); |
| } |
| else |
| #endif |
| { |
| if(!threadsAwake) |
| { |
| suspend[0]->wait(); |
| |
| threadsAwake = 1; |
| task[0].type = Task::RESUME; |
| |
| resume[0]->signal(); |
| } |
| } |
| } |
| |
| void Renderer::threadFunction(void *parameters) |
| { |
| Renderer *renderer = static_cast<Parameters*>(parameters)->renderer; |
| int threadIndex = static_cast<Parameters*>(parameters)->threadIndex; |
| |
| if(logPrecision < IEEE) |
| { |
| CPUID::setFlushToZero(true); |
| CPUID::setDenormalsAreZero(true); |
| } |
| |
| renderer->threadLoop(threadIndex); |
| } |
| |
| void Renderer::threadLoop(int threadIndex) |
| { |
| while(!exitThreads) |
| { |
| taskLoop(threadIndex); |
| |
| suspend[threadIndex]->signal(); |
| resume[threadIndex]->wait(); |
| } |
| } |
| |
| void Renderer::taskLoop(int threadIndex) |
| { |
| while(task[threadIndex].type != Task::SUSPEND) |
| { |
| scheduleTask(threadIndex); |
| executeTask(threadIndex); |
| } |
| } |
| |
| void Renderer::findAvailableTasks() |
| { |
| // Find pixel tasks |
| for(int cluster = 0; cluster < clusterCount; cluster++) |
| { |
| if(!pixelProgress[cluster].executing) |
| { |
| for(int unit = 0; unit < unitCount; unit++) |
| { |
| if(primitiveProgress[unit].references > 0) // Contains processed primitives |
| { |
| if(pixelProgress[cluster].drawCall == primitiveProgress[unit].drawCall) |
| { |
| if(pixelProgress[cluster].processedPrimitives == primitiveProgress[unit].firstPrimitive) // Previous primitives have been rendered |
| { |
| Task &task = taskQueue[qHead]; |
| task.type = Task::PIXELS; |
| task.primitiveUnit = unit; |
| task.pixelCluster = cluster; |
| |
| pixelProgress[cluster].executing = true; |
| |
| // Commit to the task queue |
| qHead = (qHead + 1) & TASK_COUNT_BITS; |
| qSize++; |
| |
| break; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Find primitive tasks |
| if(currentDraw == nextDraw) |
| { |
| return; // No more primitives to process |
| } |
| |
| for(int unit = 0; unit < unitCount; unit++) |
| { |
| DrawCall *draw = drawList[currentDraw & DRAW_COUNT_BITS]; |
| |
| int primitive = draw->primitive; |
| int count = draw->count; |
| |
| if(primitive >= count) |
| { |
| ++currentDraw; // Atomic |
| |
| if(currentDraw == nextDraw) |
| { |
| return; // No more primitives to process |
| } |
| |
| draw = drawList[currentDraw & DRAW_COUNT_BITS]; |
| } |
| |
| if(!primitiveProgress[unit].references) // Task not already being executed and not still in use by a pixel unit |
| { |
| primitive = draw->primitive; |
| count = draw->count; |
| int batch = draw->batchSize; |
| |
| primitiveProgress[unit].drawCall = currentDraw; |
| primitiveProgress[unit].firstPrimitive = primitive; |
| primitiveProgress[unit].primitiveCount = count - primitive >= batch ? batch : count - primitive; |
| |
| draw->primitive += batch; |
| |
| Task &task = taskQueue[qHead]; |
| task.type = Task::PRIMITIVES; |
| task.primitiveUnit = unit; |
| |
| primitiveProgress[unit].references = -1; |
| |
| // Commit to the task queue |
| qHead = (qHead + 1) & TASK_COUNT_BITS; |
| qSize++; |
| } |
| } |
| } |
| |
| void Renderer::scheduleTask(int threadIndex) |
| { |
| schedulerMutex.lock(); |
| |
| int curThreadsAwake = threadsAwake; |
| |
| if((int)qSize < threadCount - curThreadsAwake + 1) |
| { |
| findAvailableTasks(); |
| } |
| |
| if(qSize != 0) |
| { |
| task[threadIndex] = taskQueue[(qHead - qSize) & TASK_COUNT_BITS]; |
| qSize--; |
| |
| if(curThreadsAwake != threadCount) |
| { |
| int wakeup = qSize - curThreadsAwake + 1; |
| |
| for(int i = 0; i < threadCount && wakeup > 0; i++) |
| { |
| if(task[i].type == Task::SUSPEND) |
| { |
| suspend[i]->wait(); |
| task[i].type = Task::RESUME; |
| resume[i]->signal(); |
| |
| ++threadsAwake; // Atomic |
| wakeup--; |
| } |
| } |
| } |
| } |
| else |
| { |
| task[threadIndex].type = Task::SUSPEND; |
| |
| --threadsAwake; // Atomic |
| } |
| |
| schedulerMutex.unlock(); |
| } |
| |
| void Renderer::executeTask(int threadIndex) |
| { |
| #if PERF_HUD |
| int64_t startTick = Timer::ticks(); |
| #endif |
| |
| switch(task[threadIndex].type) |
| { |
| case Task::PRIMITIVES: |
| { |
| int unit = task[threadIndex].primitiveUnit; |
| |
| int input = primitiveProgress[unit].firstPrimitive; |
| int count = primitiveProgress[unit].primitiveCount; |
| DrawCall *draw = drawList[primitiveProgress[unit].drawCall & DRAW_COUNT_BITS]; |
| int (Renderer::*setupPrimitives)(int batch, int count) = draw->setupPrimitives; |
| |
| processPrimitiveVertices(unit, input, count, draw->count, threadIndex); |
| |
| #if PERF_HUD |
| int64_t time = Timer::ticks(); |
| vertexTime[threadIndex] += time - startTick; |
| startTick = time; |
| #endif |
| |
| int visible = 0; |
| |
| if(!draw->setupState.rasterizerDiscard) |
| { |
| visible = (this->*setupPrimitives)(unit, count); |
| } |
| |
| primitiveProgress[unit].visible = visible; |
| primitiveProgress[unit].references = clusterCount; |
| |
| #if PERF_HUD |
| setupTime[threadIndex] += Timer::ticks() - startTick; |
| #endif |
| } |
| break; |
| case Task::PIXELS: |
| { |
| int unit = task[threadIndex].primitiveUnit; |
| int visible = primitiveProgress[unit].visible; |
| |
| if(visible > 0) |
| { |
| int cluster = task[threadIndex].pixelCluster; |
| Primitive *primitive = primitiveBatch[unit]; |
| DrawCall *draw = drawList[pixelProgress[cluster].drawCall & DRAW_COUNT_BITS]; |
| DrawData *data = draw->data; |
| PixelProcessor::RoutinePointer pixelRoutine = draw->pixelPointer; |
| |
| pixelRoutine(primitive, visible, cluster, data); |
| } |
| |
| finishRendering(task[threadIndex]); |
| |
| #if PERF_HUD |
| pixelTime[threadIndex] += Timer::ticks() - startTick; |
| #endif |
| } |
| break; |
| case Task::RESUME: |
| break; |
| case Task::SUSPEND: |
| break; |
| default: |
| ASSERT(false); |
| } |
| } |
| |
| void Renderer::synchronize() |
| { |
| sync->lock(sw::PUBLIC); |
| sync->unlock(); |
| } |
| |
| void Renderer::finishRendering(Task &pixelTask) |
| { |
| int unit = pixelTask.primitiveUnit; |
| int cluster = pixelTask.pixelCluster; |
| |
| DrawCall &draw = *drawList[primitiveProgress[unit].drawCall & DRAW_COUNT_BITS]; |
| DrawData &data = *draw.data; |
| int primitive = primitiveProgress[unit].firstPrimitive; |
| int count = primitiveProgress[unit].primitiveCount; |
| int processedPrimitives = primitive + count; |
| |
| pixelProgress[cluster].processedPrimitives = processedPrimitives; |
| |
| if(pixelProgress[cluster].processedPrimitives >= draw.count) |
| { |
| ++pixelProgress[cluster].drawCall; // Atomic |
| pixelProgress[cluster].processedPrimitives = 0; |
| } |
| |
| int ref = primitiveProgress[unit].references--; // Atomic |
| |
| if(ref == 0) |
| { |
| ref = draw.references--; // Atomic |
| |
| if(ref == 0) |
| { |
| #if PERF_PROFILE |
| for(int cluster = 0; cluster < clusterCount; cluster++) |
| { |
| for(int i = 0; i < PERF_TIMERS; i++) |
| { |
| profiler.cycles[i] += data.cycles[i][cluster]; |
| } |
| } |
| #endif |
| |
| if(draw.queries) |
| { |
| for(auto &query : *(draw.queries)) |
| { |
| std::unique_lock<std::mutex> mutexLock(query->mutex); |
| |
| switch(query->type) |
| { |
| case VK_QUERY_TYPE_OCCLUSION: |
| for(int cluster = 0; cluster < clusterCount; cluster++) |
| { |
| query->data += data.occlusion[cluster]; |
| } |
| break; |
| default: |
| break; |
| } |
| |
| int queryRef = --query->reference; // Atomic |
| if(queryRef == 0) |
| { |
| query->state = vk::Query::FINISHED; |
| } |
| |
| // Manual unlocking is done before notifying, to avoid |
| // waking up the waiting thread only to block again |
| mutexLock.unlock(); |
| query->condition.notify_one(); |
| } |
| |
| delete draw.queries; |
| draw.queries = 0; |
| } |
| |
| draw.vertexRoutine->unbind(); |
| draw.setupRoutine->unbind(); |
| draw.pixelRoutine->unbind(); |
| |
| sync->unlock(); |
| |
| draw.references = -1; |
| resumeApp->signal(); |
| } |
| } |
| |
| pixelProgress[cluster].executing = false; |
| } |
| |
| void Renderer::processPrimitiveVertices(int unit, unsigned int start, unsigned int triangleCount, unsigned int loop, int thread) |
| { |
| Triangle *triangle = triangleBatch[unit]; |
| int primitiveDrawCall = primitiveProgress[unit].drawCall; |
| DrawCall *draw = drawList[primitiveDrawCall & DRAW_COUNT_BITS]; |
| DrawData *data = draw->data; |
| VertexTask *task = vertexTask[thread]; |
| |
| const void *indices = data->indices; |
| VertexProcessor::RoutinePointer vertexRoutine = draw->vertexPointer; |
| |
| if(task->vertexCache.drawCall != primitiveDrawCall) |
| { |
| task->vertexCache.clear(); |
| task->vertexCache.drawCall = primitiveDrawCall; |
| } |
| |
| unsigned int batch[128][3]; // FIXME: Adjust to dynamic batch size |
| VkPrimitiveTopology topology = static_cast<VkPrimitiveTopology>(static_cast<int>(draw->topology)); |
| |
| if(!indices) |
| { |
| struct LinearIndex |
| { |
| unsigned int operator[](unsigned int i) { return i; } |
| }; |
| |
| if(!setBatchIndices(batch, topology, LinearIndex(), start, triangleCount)) |
| { |
| return; |
| } |
| } |
| else |
| { |
| switch(draw->indexType) |
| { |
| case VK_INDEX_TYPE_UINT16: |
| if(!setBatchIndices(batch, topology, static_cast<const uint16_t*>(indices), start, triangleCount)) |
| { |
| return; |
| } |
| break; |
| case VK_INDEX_TYPE_UINT32: |
| if(!setBatchIndices(batch, topology, static_cast<const uint32_t*>(indices), start, triangleCount)) |
| { |
| return; |
| } |
| break; |
| break; |
| default: |
| ASSERT(false); |
| return; |
| } |
| } |
| |
| task->primitiveStart = start; |
| task->vertexCount = triangleCount * 3; |
| vertexRoutine(&triangle->v0, (unsigned int*)&batch, task, data); |
| } |
| |
| int Renderer::setupTriangles(int unit, int count) |
| { |
| Triangle *triangle = triangleBatch[unit]; |
| Primitive *primitive = primitiveBatch[unit]; |
| |
| DrawCall &draw = *drawList[primitiveProgress[unit].drawCall & DRAW_COUNT_BITS]; |
| SetupProcessor::State &state = draw.setupState; |
| const SetupProcessor::RoutinePointer &setupRoutine = draw.setupPointer; |
| |
| int ms = state.multiSample; |
| const DrawData *data = draw.data; |
| int visible = 0; |
| |
| for(int i = 0; i < count; i++, triangle++) |
| { |
| Vertex &v0 = triangle->v0; |
| Vertex &v1 = triangle->v1; |
| Vertex &v2 = triangle->v2; |
| |
| if((v0.clipFlags & v1.clipFlags & v2.clipFlags) == Clipper::CLIP_FINITE) |
| { |
| Polygon polygon(&v0.builtins.position, &v1.builtins.position, &v2.builtins.position); |
| |
| int clipFlagsOr = v0.clipFlags | v1.clipFlags | v2.clipFlags; |
| |
| if(clipFlagsOr != Clipper::CLIP_FINITE) |
| { |
| if(!clipper->clip(polygon, clipFlagsOr, draw)) |
| { |
| continue; |
| } |
| } |
| |
| if(setupRoutine(primitive, triangle, &polygon, data)) |
| { |
| primitive += ms; |
| visible++; |
| } |
| } |
| } |
| |
| return visible; |
| } |
| |
| int Renderer::setupLines(int unit, int count) |
| { |
| Triangle *triangle = triangleBatch[unit]; |
| Primitive *primitive = primitiveBatch[unit]; |
| int visible = 0; |
| |
| DrawCall &draw = *drawList[primitiveProgress[unit].drawCall & DRAW_COUNT_BITS]; |
| SetupProcessor::State &state = draw.setupState; |
| |
| int ms = state.multiSample; |
| |
| for(int i = 0; i < count; i++) |
| { |
| if(setupLine(*primitive, *triangle, draw)) |
| { |
| primitive += ms; |
| visible++; |
| } |
| |
| triangle++; |
| } |
| |
| return visible; |
| } |
| |
| int Renderer::setupPoints(int unit, int count) |
| { |
| Triangle *triangle = triangleBatch[unit]; |
| Primitive *primitive = primitiveBatch[unit]; |
| int visible = 0; |
| |
| DrawCall &draw = *drawList[primitiveProgress[unit].drawCall & DRAW_COUNT_BITS]; |
| SetupProcessor::State &state = draw.setupState; |
| |
| int ms = state.multiSample; |
| |
| for(int i = 0; i < count; i++) |
| { |
| if(setupPoint(*primitive, *triangle, draw)) |
| { |
| primitive += ms; |
| visible++; |
| } |
| |
| triangle++; |
| } |
| |
| return visible; |
| } |
| |
| bool Renderer::setupLine(Primitive &primitive, Triangle &triangle, const DrawCall &draw) |
| { |
| const SetupProcessor::RoutinePointer &setupRoutine = draw.setupPointer; |
| const SetupProcessor::State &state = draw.setupState; |
| const DrawData &data = *draw.data; |
| |
| float lineWidth = data.lineWidth; |
| |
| Vertex &v0 = triangle.v0; |
| Vertex &v1 = triangle.v1; |
| |
| const float4 &P0 = v0.builtins.position; |
| const float4 &P1 = v1.builtins.position; |
| |
| if(P0.w <= 0 && P1.w <= 0) |
| { |
| return false; |
| } |
| |
| const float W = data.Wx16[0] * (1.0f / 16.0f); |
| const float H = data.Hx16[0] * (1.0f / 16.0f); |
| |
| float dx = W * (P1.x / P1.w - P0.x / P0.w); |
| float dy = H * (P1.y / P1.w - P0.y / P0.w); |
| |
| if(dx == 0 && dy == 0) |
| { |
| return false; |
| } |
| |
| if(state.multiSample > 1) // Rectangle |
| { |
| float4 P[4]; |
| int C[4]; |
| |
| P[0] = P0; |
| P[1] = P1; |
| P[2] = P1; |
| P[3] = P0; |
| |
| float scale = lineWidth * 0.5f / sqrt(dx*dx + dy*dy); |
| |
| dx *= scale; |
| dy *= scale; |
| |
| float dx0h = dx * P0.w / H; |
| float dy0w = dy * P0.w / W; |
| |
| float dx1h = dx * P1.w / H; |
| float dy1w = dy * P1.w / W; |
| |
| P[0].x += -dy0w; |
| P[0].y += +dx0h; |
| C[0] = clipper->computeClipFlags(P[0]); |
| |
| P[1].x += -dy1w; |
| P[1].y += +dx1h; |
| C[1] = clipper->computeClipFlags(P[1]); |
| |
| P[2].x += +dy1w; |
| P[2].y += -dx1h; |
| C[2] = clipper->computeClipFlags(P[2]); |
| |
| P[3].x += +dy0w; |
| P[3].y += -dx0h; |
| C[3] = clipper->computeClipFlags(P[3]); |
| |
| if((C[0] & C[1] & C[2] & C[3]) == Clipper::CLIP_FINITE) |
| { |
| Polygon polygon(P, 4); |
| |
| int clipFlagsOr = C[0] | C[1] | C[2] | C[3]; |
| |
| if(clipFlagsOr != Clipper::CLIP_FINITE) |
| { |
| if(!clipper->clip(polygon, clipFlagsOr, draw)) |
| { |
| return false; |
| } |
| } |
| |
| return setupRoutine(&primitive, &triangle, &polygon, &data); |
| } |
| } |
| else // Diamond test convention |
| { |
| float4 P[8]; |
| int C[8]; |
| |
| P[0] = P0; |
| P[1] = P0; |
| P[2] = P0; |
| P[3] = P0; |
| P[4] = P1; |
| P[5] = P1; |
| P[6] = P1; |
| P[7] = P1; |
| |
| float dx0 = lineWidth * 0.5f * P0.w / W; |
| float dy0 = lineWidth * 0.5f * P0.w / H; |
| |
| float dx1 = lineWidth * 0.5f * P1.w / W; |
| float dy1 = lineWidth * 0.5f * P1.w / H; |
| |
| P[0].x += -dx0; |
| C[0] = clipper->computeClipFlags(P[0]); |
| |
| P[1].y += +dy0; |
| C[1] = clipper->computeClipFlags(P[1]); |
| |
| P[2].x += +dx0; |
| C[2] = clipper->computeClipFlags(P[2]); |
| |
| P[3].y += -dy0; |
| C[3] = clipper->computeClipFlags(P[3]); |
| |
| P[4].x += -dx1; |
| C[4] = clipper->computeClipFlags(P[4]); |
| |
| P[5].y += +dy1; |
| C[5] = clipper->computeClipFlags(P[5]); |
| |
| P[6].x += +dx1; |
| C[6] = clipper->computeClipFlags(P[6]); |
| |
| P[7].y += -dy1; |
| C[7] = clipper->computeClipFlags(P[7]); |
| |
| if((C[0] & C[1] & C[2] & C[3] & C[4] & C[5] & C[6] & C[7]) == Clipper::CLIP_FINITE) |
| { |
| float4 L[6]; |
| |
| if(dx > -dy) |
| { |
| if(dx > dy) // Right |
| { |
| L[0] = P[0]; |
| L[1] = P[1]; |
| L[2] = P[5]; |
| L[3] = P[6]; |
| L[4] = P[7]; |
| L[5] = P[3]; |
| } |
| else // Down |
| { |
| L[0] = P[0]; |
| L[1] = P[4]; |
| L[2] = P[5]; |
| L[3] = P[6]; |
| L[4] = P[2]; |
| L[5] = P[3]; |
| } |
| } |
| else |
| { |
| if(dx > dy) // Up |
| { |
| L[0] = P[0]; |
| L[1] = P[1]; |
| L[2] = P[2]; |
| L[3] = P[6]; |
| L[4] = P[7]; |
| L[5] = P[4]; |
| } |
| else // Left |
| { |
| L[0] = P[1]; |
| L[1] = P[2]; |
| L[2] = P[3]; |
| L[3] = P[7]; |
| L[4] = P[4]; |
| L[5] = P[5]; |
| } |
| } |
| |
| Polygon polygon(L, 6); |
| |
| int clipFlagsOr = C[0] | C[1] | C[2] | C[3] | C[4] | C[5] | C[6] | C[7]; |
| |
| if(clipFlagsOr != Clipper::CLIP_FINITE) |
| { |
| if(!clipper->clip(polygon, clipFlagsOr, draw)) |
| { |
| return false; |
| } |
| } |
| |
| return setupRoutine(&primitive, &triangle, &polygon, &data); |
| } |
| } |
| |
| return false; |
| } |
| |
| bool Renderer::setupPoint(Primitive &primitive, Triangle &triangle, const DrawCall &draw) |
| { |
| const SetupProcessor::RoutinePointer &setupRoutine = draw.setupPointer; |
| const DrawData &data = *draw.data; |
| |
| Vertex &v = triangle.v0; |
| |
| float pSize = v.builtins.pointSize; |
| |
| pSize = clamp(pSize, 1.0f, static_cast<float>(vk::MAX_POINT_SIZE)); |
| |
| float4 P[4]; |
| int C[4]; |
| |
| P[0] = v.builtins.position; |
| P[1] = v.builtins.position; |
| P[2] = v.builtins.position; |
| P[3] = v.builtins.position; |
| |
| const float X = pSize * P[0].w * data.halfPixelX[0]; |
| const float Y = pSize * P[0].w * data.halfPixelY[0]; |
| |
| P[0].x -= X; |
| P[0].y += Y; |
| C[0] = clipper->computeClipFlags(P[0]); |
| |
| P[1].x += X; |
| P[1].y += Y; |
| C[1] = clipper->computeClipFlags(P[1]); |
| |
| P[2].x += X; |
| P[2].y -= Y; |
| C[2] = clipper->computeClipFlags(P[2]); |
| |
| P[3].x -= X; |
| P[3].y -= Y; |
| C[3] = clipper->computeClipFlags(P[3]); |
| |
| triangle.v1 = triangle.v0; |
| triangle.v2 = triangle.v0; |
| |
| triangle.v1.projected.x += iround(16 * 0.5f * pSize); |
| triangle.v2.projected.y -= iround(16 * 0.5f * pSize) * (data.Hx16[0] > 0.0f ? 1 : -1); // Both Direct3D and OpenGL expect (0, 0) in the top-left corner |
| |
| Polygon polygon(P, 4); |
| |
| if((C[0] & C[1] & C[2] & C[3]) == Clipper::CLIP_FINITE) |
| { |
| int clipFlagsOr = C[0] | C[1] | C[2] | C[3]; |
| |
| if(clipFlagsOr != Clipper::CLIP_FINITE) |
| { |
| if(!clipper->clip(polygon, clipFlagsOr, draw)) |
| { |
| return false; |
| } |
| } |
| |
| return setupRoutine(&primitive, &triangle, &polygon, &data); |
| } |
| |
| return false; |
| } |
| |
| void Renderer::initializeThreads() |
| { |
| unitCount = ceilPow2(threadCount); |
| clusterCount = ceilPow2(threadCount); |
| |
| for(int i = 0; i < unitCount; i++) |
| { |
| triangleBatch[i] = (Triangle*)allocate(batchSize * sizeof(Triangle)); |
| primitiveBatch[i] = (Primitive*)allocate(batchSize * sizeof(Primitive)); |
| } |
| |
| for(int i = 0; i < threadCount; i++) |
| { |
| vertexTask[i] = (VertexTask*)allocate(sizeof(VertexTask)); |
| vertexTask[i]->vertexCache.drawCall = -1; |
| |
| task[i].type = Task::SUSPEND; |
| |
| resume[i] = new Event(); |
| suspend[i] = new Event(); |
| |
| Parameters parameters; |
| parameters.threadIndex = i; |
| parameters.renderer = this; |
| |
| exitThreads = false; |
| worker[i] = new Thread(threadFunction, ¶meters); |
| |
| suspend[i]->wait(); |
| suspend[i]->signal(); |
| } |
| } |
| |
| void Renderer::terminateThreads() |
| { |
| while(threadsAwake != 0) |
| { |
| Thread::sleep(1); |
| } |
| |
| for(int thread = 0; thread < threadCount; thread++) |
| { |
| if(worker[thread]) |
| { |
| exitThreads = true; |
| resume[thread]->signal(); |
| worker[thread]->join(); |
| |
| delete worker[thread]; |
| worker[thread] = 0; |
| delete resume[thread]; |
| resume[thread] = 0; |
| delete suspend[thread]; |
| suspend[thread] = 0; |
| } |
| |
| deallocate(vertexTask[thread]); |
| vertexTask[thread] = 0; |
| } |
| |
| for(int i = 0; i < 16; i++) |
| { |
| deallocate(triangleBatch[i]); |
| triangleBatch[i] = 0; |
| |
| deallocate(primitiveBatch[i]); |
| primitiveBatch[i] = 0; |
| } |
| } |
| |
| void Renderer::setMultiSampleMask(unsigned int mask) |
| { |
| context->sampleMask = mask; |
| } |
| |
| void Renderer::setTransparencyAntialiasing(TransparencyAntialiasing transparencyAntialiasing) |
| { |
| sw::transparencyAntialiasing = transparencyAntialiasing; |
| } |
| |
| void Renderer::setLineWidth(float width) |
| { |
| context->lineWidth = width; |
| } |
| |
| void Renderer::setDepthBias(float bias) |
| { |
| context->depthBias = bias; |
| } |
| |
| void Renderer::setSlopeDepthBias(float slopeBias) |
| { |
| context->slopeDepthBias = slopeBias; |
| } |
| |
| void Renderer::setRasterizerDiscard(bool rasterizerDiscard) |
| { |
| context->rasterizerDiscard = rasterizerDiscard; |
| } |
| |
| void Renderer::setPixelShader(const SpirvShader *shader) |
| { |
| context->pixelShader = shader; |
| } |
| |
| void Renderer::setVertexShader(const SpirvShader *shader) |
| { |
| context->vertexShader = shader; |
| } |
| |
| void Renderer::addQuery(vk::Query *query) |
| { |
| queries.push_back(query); |
| } |
| |
| void Renderer::removeQuery(vk::Query *query) |
| { |
| queries.remove(query); |
| } |
| |
| void Renderer::advanceInstanceAttributes() |
| { |
| for(uint32_t i = 0; i < vk::MAX_VERTEX_INPUT_BINDINGS; i++) |
| { |
| auto &attrib = context->input[i]; |
| if (attrib.count && attrib.instanceStride) |
| { |
| // Under the casts: attrib.buffer += attrib.instanceStride |
| attrib.buffer = (void const *)((uintptr_t)attrib.buffer + attrib.instanceStride); |
| } |
| } |
| } |
| |
| #if PERF_HUD |
| int Renderer::getThreadCount() |
| { |
| return threadCount; |
| } |
| |
| int64_t Renderer::getVertexTime(int thread) |
| { |
| return vertexTime[thread]; |
| } |
| |
| int64_t Renderer::getSetupTime(int thread) |
| { |
| return setupTime[thread]; |
| } |
| |
| int64_t Renderer::getPixelTime(int thread) |
| { |
| return pixelTime[thread]; |
| } |
| |
| void Renderer::resetTimers() |
| { |
| for(int thread = 0; thread < threadCount; thread++) |
| { |
| vertexTime[thread] = 0; |
| setupTime[thread] = 0; |
| pixelTime[thread] = 0; |
| } |
| } |
| #endif |
| |
| void Renderer::setContext(const sw::Context& context) |
| { |
| *(this->context) = context; |
| } |
| |
| void Renderer::setViewport(const VkViewport &viewport) |
| { |
| this->viewport = viewport; |
| } |
| |
| void Renderer::setScissor(const VkRect2D &scissor) |
| { |
| this->scissor = scissor; |
| } |
| |
| void Renderer::updateConfiguration(bool initialUpdate) |
| { |
| bool newConfiguration = swiftConfig->hasNewConfiguration(); |
| |
| if(newConfiguration || initialUpdate) |
| { |
| terminateThreads(); |
| |
| SwiftConfig::Configuration configuration = {}; |
| swiftConfig->getConfiguration(configuration); |
| |
| precacheVertex = !newConfiguration && configuration.precache; |
| precacheSetup = !newConfiguration && configuration.precache; |
| precachePixel = !newConfiguration && configuration.precache; |
| |
| VertexProcessor::setRoutineCacheSize(configuration.vertexRoutineCacheSize); |
| PixelProcessor::setRoutineCacheSize(configuration.pixelRoutineCacheSize); |
| SetupProcessor::setRoutineCacheSize(configuration.setupRoutineCacheSize); |
| |
| switch(configuration.textureSampleQuality) |
| { |
| case 0: Sampler::setFilterQuality(FILTER_POINT); break; |
| case 1: Sampler::setFilterQuality(FILTER_LINEAR); break; |
| case 2: Sampler::setFilterQuality(FILTER_ANISOTROPIC); break; |
| default: Sampler::setFilterQuality(FILTER_ANISOTROPIC); break; |
| } |
| |
| switch(configuration.mipmapQuality) |
| { |
| case 0: Sampler::setMipmapQuality(MIPMAP_POINT); break; |
| case 1: Sampler::setMipmapQuality(MIPMAP_LINEAR); break; |
| default: Sampler::setMipmapQuality(MIPMAP_LINEAR); break; |
| } |
| |
| setPerspectiveCorrection(configuration.perspectiveCorrection); |
| |
| switch(configuration.transcendentalPrecision) |
| { |
| case 0: |
| logPrecision = APPROXIMATE; |
| expPrecision = APPROXIMATE; |
| rcpPrecision = APPROXIMATE; |
| rsqPrecision = APPROXIMATE; |
| break; |
| case 1: |
| logPrecision = PARTIAL; |
| expPrecision = PARTIAL; |
| rcpPrecision = PARTIAL; |
| rsqPrecision = PARTIAL; |
| break; |
| case 2: |
| logPrecision = ACCURATE; |
| expPrecision = ACCURATE; |
| rcpPrecision = ACCURATE; |
| rsqPrecision = ACCURATE; |
| break; |
| case 3: |
| logPrecision = WHQL; |
| expPrecision = WHQL; |
| rcpPrecision = WHQL; |
| rsqPrecision = WHQL; |
| break; |
| case 4: |
| logPrecision = IEEE; |
| expPrecision = IEEE; |
| rcpPrecision = IEEE; |
| rsqPrecision = IEEE; |
| break; |
| default: |
| logPrecision = ACCURATE; |
| expPrecision = ACCURATE; |
| rcpPrecision = ACCURATE; |
| rsqPrecision = ACCURATE; |
| break; |
| } |
| |
| switch(configuration.transparencyAntialiasing) |
| { |
| case 0: transparencyAntialiasing = TRANSPARENCY_NONE; break; |
| case 1: transparencyAntialiasing = TRANSPARENCY_ALPHA_TO_COVERAGE; break; |
| default: transparencyAntialiasing = TRANSPARENCY_NONE; break; |
| } |
| |
| switch(configuration.threadCount) |
| { |
| case -1: threadCount = CPUID::coreCount(); break; |
| case 0: threadCount = CPUID::processAffinity(); break; |
| default: threadCount = configuration.threadCount; break; |
| } |
| |
| CPUID::setEnableSSE4_1(configuration.enableSSE4_1); |
| CPUID::setEnableSSSE3(configuration.enableSSSE3); |
| CPUID::setEnableSSE3(configuration.enableSSE3); |
| CPUID::setEnableSSE2(configuration.enableSSE2); |
| CPUID::setEnableSSE(configuration.enableSSE); |
| |
| for(int pass = 0; pass < 10; pass++) |
| { |
| optimization[pass] = configuration.optimization[pass]; |
| } |
| |
| forceWindowed = configuration.forceWindowed; |
| postBlendSRGB = configuration.postBlendSRGB; |
| exactColorRounding = configuration.exactColorRounding; |
| forceClearRegisters = configuration.forceClearRegisters; |
| |
| #ifndef NDEBUG |
| minPrimitives = configuration.minPrimitives; |
| maxPrimitives = configuration.maxPrimitives; |
| #endif |
| } |
| |
| if(!initialUpdate && !worker[0]) |
| { |
| initializeThreads(); |
| } |
| } |
| } |