particle simulation fixes, editor updates
This commit is contained in:
@@ -1309,6 +1309,8 @@ void EditorComponent::Update(float dt)
|
||||
SAFE_DELETE(history);
|
||||
}
|
||||
|
||||
emitterWnd->UpdateData();
|
||||
|
||||
__super::Update(dt);
|
||||
|
||||
renderPath->Update(dt);
|
||||
|
||||
+59
-10
@@ -54,11 +54,6 @@ EmitterWindow::EmitterWindow(wiGUI* gui) : GUI(gui)
|
||||
if (emitter != nullptr)
|
||||
{
|
||||
emitter->SetMaxParticleCount((uint32_t)args.iValue);
|
||||
|
||||
stringstream ss("");
|
||||
ss.precision(2);
|
||||
ss << "Memort Budget: " << emitter->GetMemorySizeInBytes() / 1024.0f / 1024.0f << " MB";
|
||||
memoryBudgetLabel->SetText(ss.str());
|
||||
}
|
||||
});
|
||||
maxParticlesSlider->SetEnabled(false);
|
||||
@@ -195,6 +190,29 @@ EmitterWindow::EmitterWindow(wiGUI* gui) : GUI(gui)
|
||||
emitMotionBlurSlider->SetTooltip("Set the motion blur amount for the particle system.");
|
||||
emitterWindow->AddWidget(emitMotionBlurSlider);
|
||||
|
||||
y += 30;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
debugCheckBox = new wiCheckBox("DEBUG: ");
|
||||
debugCheckBox->SetPos(XMFLOAT2(x, y += step));
|
||||
debugCheckBox->OnClick([&](wiEventArgs args) {
|
||||
auto emitter = GetEmitter();
|
||||
if (emitter != nullptr)
|
||||
{
|
||||
emitter->DEBUG = args.bValue;
|
||||
}
|
||||
});
|
||||
debugCheckBox->SetCheck(false);
|
||||
debugCheckBox->SetTooltip("Enable debug info for the emitter. This involves reading back GPU data, so rendering can slow down.");
|
||||
emitterWindow->AddWidget(debugCheckBox);
|
||||
|
||||
debugDataLabel = new wiLabel("EmitterDebugDataLabel");
|
||||
debugDataLabel->SetText("Enable DEBUG to view debugging data");
|
||||
debugDataLabel->SetSize(XMFLOAT2(300, 100));
|
||||
debugDataLabel->SetPos(XMFLOAT2(x + step, y));
|
||||
emitterWindow->AddWidget(debugDataLabel);
|
||||
|
||||
|
||||
emitterWindow->Translate(XMFLOAT3(200, 50, 0));
|
||||
emitterWindow->SetVisible(false);
|
||||
@@ -212,6 +230,12 @@ EmitterWindow::~EmitterWindow()
|
||||
|
||||
void EmitterWindow::SetObject(Object* obj)
|
||||
{
|
||||
// first try to turn off any debug readbacks for emitters:
|
||||
if (GetEmitter() != nullptr)
|
||||
{
|
||||
GetEmitter()->DEBUG = false;
|
||||
}
|
||||
|
||||
object = obj;
|
||||
|
||||
emitterComboBox->ClearItems();
|
||||
@@ -229,11 +253,6 @@ void EmitterWindow::SetObject(Object* obj)
|
||||
{
|
||||
maxParticlesSlider->SetValue((float)emitter->GetMaxParticleCount());
|
||||
|
||||
stringstream ss("");
|
||||
ss.precision(2);
|
||||
ss << "Memort Budget: " << emitter->GetMemorySizeInBytes() / 1024.0f / 1024.0f << " MB";
|
||||
memoryBudgetLabel->SetText(ss.str());
|
||||
|
||||
emitCountSlider->SetValue(emitter->count);
|
||||
emitSizeSlider->SetValue(emitter->size);
|
||||
emitRotationSlider->SetValue(emitter->rotation);
|
||||
@@ -277,3 +296,33 @@ wiEmittedParticle* EmitterWindow::GetEmitter()
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void EmitterWindow::UpdateData()
|
||||
{
|
||||
auto emitter = GetEmitter();
|
||||
if (emitter == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (emitter->DEBUG)
|
||||
{
|
||||
auto data = emitter->GetDebugData();
|
||||
|
||||
stringstream ss("");
|
||||
ss << "Alive Particle Count [CURRENT] = " << data.aliveCount_CURRENT << endl;
|
||||
ss << "Alive Particle Count [NEW] = " << data.aliveCount_NEW << endl;
|
||||
ss << "Dead Particle Count = " << data.deadCount << endl;
|
||||
ss << "GPU Emit count = " << data.realEmitCount << endl;
|
||||
debugDataLabel->SetText(ss.str());
|
||||
}
|
||||
else
|
||||
{
|
||||
debugDataLabel->SetText("Enable DEBUG to view debugging data");
|
||||
}
|
||||
|
||||
stringstream ss("");
|
||||
ss.precision(2);
|
||||
ss << "Memort Budget: " << emitter->GetMemorySizeInBytes() / 1024.0f / 1024.0f << " MB";
|
||||
memoryBudgetLabel->SetText(ss.str());
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public:
|
||||
~EmitterWindow();
|
||||
|
||||
void SetObject(Object* obj);
|
||||
void UpdateData();
|
||||
|
||||
Object* object;
|
||||
wiEmittedParticle* GetEmitter();
|
||||
@@ -38,5 +39,8 @@ public:
|
||||
wiSlider* emitRandomnessSlider;
|
||||
wiSlider* emitLifeRandomnessSlider;
|
||||
wiSlider* emitMotionBlurSlider;
|
||||
|
||||
wiCheckBox* debugCheckBox;
|
||||
wiLabel* debugDataLabel;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
This file contains changelog of wiArchive versions
|
||||
|
||||
11: serialize additional emitter properties
|
||||
10: serialize force fields
|
||||
9: refactored emitter particle system
|
||||
8: refactored vertex wind weight
|
||||
|
||||
@@ -17,6 +17,13 @@ struct Particle
|
||||
float2 mirror;
|
||||
};
|
||||
|
||||
struct ParticleCounters
|
||||
{
|
||||
uint aliveCount_CURRENT;
|
||||
uint deadCount;
|
||||
uint aliveCount_NEW;
|
||||
uint realEmitCount;
|
||||
};
|
||||
|
||||
CBUFFER(EmittedParticleCB, CBSLOT_OTHER_EMITTEDPARTICLE)
|
||||
{
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
#include "globals.hlsli"
|
||||
#include "ShaderInterop_EmittedParticle.h"
|
||||
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, uint4, 4); // alive count, dead count, new alive count
|
||||
RWRAWBUFFER(indirectDrawBuffer, 6); // indirectdrawinstanced args
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, ParticleCounters, 4);
|
||||
RWRAWBUFFER(indirectDrawBuffer, 5); // indirectdrawinstanced args
|
||||
|
||||
[numthreads(1, 1, 1)]
|
||||
void main(uint3 DTid : SV_DispatchThreadID)
|
||||
{
|
||||
uint aliveCount_NEW = counterBuffer[0][2];
|
||||
|
||||
// Fill Draw arguments for drawing:
|
||||
// VertexCountPerInstance;
|
||||
// InstanceCount;
|
||||
// StartVertexLocation;
|
||||
// StartInstanceLocation;
|
||||
indirectDrawBuffer.Store4(0, uint4(aliveCount_NEW * 6, 1, 0, 0));
|
||||
indirectDrawBuffer.Store4(24, uint4(counterBuffer[0].aliveCount_NEW * 6, 1, 0, 0));
|
||||
}
|
||||
@@ -5,7 +5,7 @@ RWSTRUCTUREDBUFFER(particleBuffer, Particle, 0);
|
||||
RWSTRUCTUREDBUFFER(aliveBuffer_OLD, uint, 1);
|
||||
RWSTRUCTUREDBUFFER(aliveBuffer_NEW, uint, 2);
|
||||
RWSTRUCTUREDBUFFER(deadBuffer, uint, 3);
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, uint4, 4);
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, ParticleCounters, 4);
|
||||
|
||||
TEXTURE2D(randomTex, float4, TEXSLOT_ONDEMAND0);
|
||||
TYPEDBUFFER(meshIndexBuffer, uint, TEXSLOT_ONDEMAND1);
|
||||
@@ -16,7 +16,7 @@ RAWBUFFER(meshVertexBuffer_NOR, TEXSLOT_ONDEMAND3);
|
||||
[numthreads(THREADCOUNT_EMIT, 1, 1)]
|
||||
void main(uint3 DTid : SV_DispatchThreadID)
|
||||
{
|
||||
uint emitCount = counterBuffer[0][3];
|
||||
uint emitCount = counterBuffer[0].realEmitCount;
|
||||
|
||||
if(DTid.x < emitCount)
|
||||
{
|
||||
@@ -92,7 +92,7 @@ void main(uint3 DTid : SV_DispatchThreadID)
|
||||
|
||||
// new particle index retrieved from dead list (pop):
|
||||
uint deadCount;
|
||||
InterlockedAdd(counterBuffer[0][1], -1, deadCount);
|
||||
InterlockedAdd(counterBuffer[0].deadCount, -1, deadCount);
|
||||
uint newParticleIndex = deadBuffer[deadCount - 1];
|
||||
|
||||
// write out the new particle:
|
||||
@@ -100,7 +100,7 @@ void main(uint3 DTid : SV_DispatchThreadID)
|
||||
|
||||
// and add index to the alive list (push):
|
||||
uint aliveCount_OLD;
|
||||
InterlockedAdd(counterBuffer[0][0], 1, aliveCount_OLD);
|
||||
InterlockedAdd(counterBuffer[0].aliveCount_CURRENT, 1, aliveCount_OLD);
|
||||
aliveBuffer_OLD[aliveCount_OLD] = newParticleIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "globals.hlsli"
|
||||
#include "ShaderInterop_EmittedParticle.h"
|
||||
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, uint4, 4); // alive count, dead count, new alive count, realEmitCount
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, ParticleCounters, 4);
|
||||
RWRAWBUFFER(indirectDispatchBuffer, 5); // indirect kickoff args
|
||||
|
||||
[numthreads(1, 1, 1)]
|
||||
void main(uint3 DTid : SV_DispatchThreadID)
|
||||
{
|
||||
uint deadCount = counterBuffer[0][1];
|
||||
uint deadCount = counterBuffer[0].deadCount;
|
||||
|
||||
// we can not emit more than there are free slots in the dead list:
|
||||
uint realEmitCount = min(deadCount, xEmitCount);
|
||||
@@ -15,12 +15,12 @@ void main(uint3 DTid : SV_DispatchThreadID)
|
||||
// Fill dispatch argument buffer for simulation:
|
||||
indirectDispatchBuffer.Store3(0, uint3(ceil((float)realEmitCount / (float)THREADCOUNT_EMIT), 1, 1));
|
||||
|
||||
// copy new alivelistcount to oldalivelistcount:
|
||||
counterBuffer[0][0] = counterBuffer[0][2];
|
||||
// copy new alivelistcount to current alivelistcount:
|
||||
counterBuffer[0].aliveCount_CURRENT = counterBuffer[0].aliveCount_NEW;
|
||||
|
||||
// also reset the new alive list count:
|
||||
counterBuffer[0][2] = 0;
|
||||
counterBuffer[0].aliveCount_NEW = 0;
|
||||
|
||||
// and write real emit count:
|
||||
counterBuffer[0][3] = realEmitCount;
|
||||
counterBuffer[0].realEmitCount = realEmitCount;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ RWSTRUCTUREDBUFFER(particleBuffer, Particle, 0);
|
||||
RWSTRUCTUREDBUFFER(aliveBuffer_OLD, uint, 1);
|
||||
RWSTRUCTUREDBUFFER(aliveBuffer_NEW, uint, 2);
|
||||
RWSTRUCTUREDBUFFER(deadBuffer, uint, 3);
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, uint4, 4);
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, ParticleCounters, 4);
|
||||
|
||||
#define NUM_LDS_FORCEFIELDS 32
|
||||
struct LDS_ForceField
|
||||
@@ -26,7 +26,7 @@ void main(uint3 DTid : SV_DispatchThreadID, uint Gid : SV_GroupIndex)
|
||||
// Read alive particle count and store in LDS:
|
||||
if (Gid == 0)
|
||||
{
|
||||
aliveParticleCount = counterBuffer[0][0];
|
||||
aliveParticleCount = counterBuffer[0].aliveCount_CURRENT;
|
||||
}
|
||||
|
||||
// Load the forcefields into LDS:
|
||||
@@ -91,14 +91,14 @@ void main(uint3 DTid : SV_DispatchThreadID, uint Gid : SV_GroupIndex)
|
||||
|
||||
// add to new alive list:
|
||||
uint newAliveIndex;
|
||||
InterlockedAdd(counterBuffer[0][2], 1, newAliveIndex);
|
||||
InterlockedAdd(counterBuffer[0].aliveCount_NEW, 1, newAliveIndex);
|
||||
aliveBuffer_NEW[newAliveIndex] = particleIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
// kill:
|
||||
uint deadIndex;
|
||||
InterlockedAdd(counterBuffer[0][1], 1, deadIndex);
|
||||
InterlockedAdd(counterBuffer[0].deadCount, 1, deadIndex);
|
||||
deadBuffer[deadIndex] = particleIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
#include "globals.hlsli"
|
||||
#include "ShaderInterop_EmittedParticle.h"
|
||||
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, uint4, 4); // alive count, dead count, new alive count, realEmitCount
|
||||
RWRAWBUFFER(indirectDispatchBuffer, 5); // indirectdispatch args
|
||||
RWSTRUCTUREDBUFFER(counterBuffer, ParticleCounters, 4);
|
||||
RWRAWBUFFER(indirectDispatchBuffer, 5); // indirect simulation args
|
||||
|
||||
[numthreads(1, 1, 1)]
|
||||
void main( uint3 DTid : SV_DispatchThreadID )
|
||||
{
|
||||
uint aliveCount_OLD = counterBuffer[0][0];
|
||||
|
||||
// Fill dispatch argument buffer for simulation:
|
||||
indirectDispatchBuffer.Store3(0, uint3(ceil((float)aliveCount_OLD / (float)THREADCOUNT_SIMULATION), 1, 1));
|
||||
indirectDispatchBuffer.Store3(12, uint3(ceil((float)counterBuffer[0].aliveCount_CURRENT / (float)THREADCOUNT_SIMULATION), 1, 1));
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
using namespace std;
|
||||
|
||||
// this should always be only INCREMENTED and only if a new serialization is implemeted somewhere!
|
||||
uint64_t __archiveVersion = 10;
|
||||
uint64_t __archiveVersion = 11;
|
||||
// this is the version number of which below the archive is not compatible with the current version
|
||||
uint64_t __archiveVersionBarrier = 1;
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ wiEmittedParticle::wiEmittedParticle()
|
||||
SAFE_INIT(aliveList[1]);
|
||||
SAFE_INIT(deadList);
|
||||
SAFE_INIT(counterBuffer);
|
||||
SAFE_INIT(indirectDispatchBuffer);
|
||||
SAFE_INIT(indirectDrawBuffer);
|
||||
SAFE_INIT(indirectBuffers);
|
||||
SAFE_INIT(constantBuffer);
|
||||
SAFE_INIT(debugDataReadbackBuffer);
|
||||
|
||||
SetMaxParticleCount(10000);
|
||||
}
|
||||
@@ -91,9 +91,9 @@ wiEmittedParticle::wiEmittedParticle(const std::string& newName, const std::stri
|
||||
SAFE_INIT(aliveList[1]);
|
||||
SAFE_INIT(deadList);
|
||||
SAFE_INIT(counterBuffer);
|
||||
SAFE_INIT(indirectDispatchBuffer);
|
||||
SAFE_INIT(indirectDrawBuffer);
|
||||
SAFE_INIT(indirectBuffers);
|
||||
SAFE_INIT(constantBuffer);
|
||||
SAFE_INIT(debugDataReadbackBuffer);
|
||||
|
||||
SetMaxParticleCount(10000);
|
||||
}
|
||||
@@ -121,38 +121,44 @@ wiEmittedParticle::wiEmittedParticle(const wiEmittedParticle& other)
|
||||
SAFE_INIT(aliveList[1]);
|
||||
SAFE_INIT(deadList);
|
||||
SAFE_INIT(counterBuffer);
|
||||
SAFE_INIT(indirectDispatchBuffer);
|
||||
SAFE_INIT(indirectDrawBuffer);
|
||||
SAFE_INIT(indirectBuffers);
|
||||
SAFE_INIT(constantBuffer);
|
||||
SAFE_INIT(debugDataReadbackBuffer);
|
||||
|
||||
SetMaxParticleCount(other.GetMaxParticleCount());
|
||||
}
|
||||
|
||||
void wiEmittedParticle::SetMaxParticleCount(uint32_t value)
|
||||
{
|
||||
buffersUpToDate = false;
|
||||
MAX_PARTICLES = value;
|
||||
CreateSelfBuffers();
|
||||
}
|
||||
|
||||
void wiEmittedParticle::CreateSelfBuffers()
|
||||
{
|
||||
if (buffersUpToDate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
buffersUpToDate = true;
|
||||
|
||||
SAFE_DELETE(particleBuffer);
|
||||
SAFE_DELETE(aliveList[0]);
|
||||
SAFE_DELETE(aliveList[1]);
|
||||
SAFE_DELETE(deadList);
|
||||
SAFE_DELETE(counterBuffer);
|
||||
SAFE_DELETE(indirectDispatchBuffer);
|
||||
SAFE_DELETE(indirectDrawBuffer);
|
||||
SAFE_DELETE(indirectBuffers);
|
||||
SAFE_DELETE(constantBuffer);
|
||||
SAFE_DELETE(debugDataReadbackBuffer);
|
||||
|
||||
particleBuffer = new GPUBuffer;
|
||||
aliveList[0] = new GPUBuffer;
|
||||
aliveList[1] = new GPUBuffer;
|
||||
deadList = new GPUBuffer;
|
||||
counterBuffer = new GPUBuffer;
|
||||
indirectDispatchBuffer = new GPUBuffer;
|
||||
indirectDrawBuffer = new GPUBuffer;
|
||||
indirectBuffers = new GPUBuffer;
|
||||
constantBuffer = new GPUBuffer;
|
||||
debugDataReadbackBuffer = new GPUBuffer;
|
||||
|
||||
GPUBufferDesc bd;
|
||||
bd.Usage = USAGE_DEFAULT;
|
||||
@@ -182,8 +188,13 @@ void wiEmittedParticle::CreateSelfBuffers()
|
||||
data.pSysMem = nullptr;
|
||||
|
||||
|
||||
uint32_t counters[] = { 0, MAX_PARTICLES, 0, 0 }; // aliveCount, deadCount, newAliveCount, realEmitCount
|
||||
data.pSysMem = counters;
|
||||
ParticleCounters counters;
|
||||
counters.aliveCount_CURRENT = 0;
|
||||
counters.deadCount = MAX_PARTICLES;
|
||||
counters.aliveCount_NEW = 0;
|
||||
counters.realEmitCount = 0;
|
||||
|
||||
data.pSysMem = &counters;
|
||||
bd.ByteWidth = sizeof(counters);
|
||||
bd.StructureByteStride = sizeof(counters);
|
||||
wiRenderer::GetDevice()->CreateBuffer(&bd, &data, counterBuffer);
|
||||
@@ -192,10 +203,8 @@ void wiEmittedParticle::CreateSelfBuffers()
|
||||
|
||||
bd.BindFlags = BIND_UNORDERED_ACCESS;
|
||||
bd.MiscFlags = RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS | RESOURCE_MISC_DRAWINDIRECT_ARGS;
|
||||
bd.ByteWidth = sizeof(wiGraphicsTypes::IndirectDrawArgsInstanced);
|
||||
wiRenderer::GetDevice()->CreateBuffer(&bd, nullptr, indirectDrawBuffer);
|
||||
bd.ByteWidth = sizeof(wiGraphicsTypes::IndirectDispatchArgs);
|
||||
wiRenderer::GetDevice()->CreateBuffer(&bd, nullptr, indirectDispatchBuffer);
|
||||
bd.ByteWidth = sizeof(wiGraphicsTypes::IndirectDispatchArgs) + sizeof(wiGraphicsTypes::IndirectDispatchArgs) + sizeof(wiGraphicsTypes::IndirectDrawArgsInstanced);
|
||||
wiRenderer::GetDevice()->CreateBuffer(&bd, nullptr, indirectBuffers);
|
||||
|
||||
|
||||
bd.Usage = USAGE_DYNAMIC;
|
||||
@@ -204,6 +213,14 @@ void wiEmittedParticle::CreateSelfBuffers()
|
||||
bd.CPUAccessFlags = CPU_ACCESS_WRITE;
|
||||
bd.MiscFlags = 0;
|
||||
wiRenderer::GetDevice()->CreateBuffer(&bd, nullptr, constantBuffer);
|
||||
|
||||
{
|
||||
GPUBufferDesc debugBufDesc = counterBuffer->GetDesc();
|
||||
debugBufDesc.Usage = USAGE_STAGING;
|
||||
debugBufDesc.CPUAccessFlags = CPU_ACCESS_READ;
|
||||
debugBufDesc.BindFlags = 0;
|
||||
wiRenderer::GetDevice()->CreateBuffer(&debugBufDesc, nullptr, debugDataReadbackBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t wiEmittedParticle::GetMemorySizeInBytes() const
|
||||
@@ -215,8 +232,7 @@ uint32_t wiEmittedParticle::GetMemorySizeInBytes() const
|
||||
retVal += aliveList[1]->GetDesc().ByteWidth;
|
||||
retVal += deadList->GetDesc().ByteWidth;
|
||||
retVal += counterBuffer->GetDesc().ByteWidth;
|
||||
retVal += indirectDispatchBuffer->GetDesc().ByteWidth;
|
||||
retVal += indirectDrawBuffer->GetDesc().ByteWidth;
|
||||
retVal += indirectBuffers->GetDesc().ByteWidth;
|
||||
retVal += constantBuffer->GetDesc().ByteWidth;
|
||||
|
||||
return retVal;
|
||||
@@ -240,6 +256,8 @@ void wiEmittedParticle::Burst(float num)
|
||||
|
||||
void wiEmittedParticle::UpdateRenderData(GRAPHICSTHREAD threadID)
|
||||
{
|
||||
CreateSelfBuffers();
|
||||
|
||||
GraphicsDevice* device = wiRenderer::GetDevice();
|
||||
device->EventBegin("UpdateEmittedParticles", threadID);
|
||||
|
||||
@@ -265,12 +283,11 @@ void wiEmittedParticle::UpdateRenderData(GRAPHICSTHREAD threadID)
|
||||
|
||||
const GPUUnorderedResource* uavs[] = {
|
||||
static_cast<GPUUnorderedResource*>(particleBuffer),
|
||||
static_cast<GPUUnorderedResource*>(aliveList[0]), // last alivelist
|
||||
static_cast<GPUUnorderedResource*>(aliveList[1]), // new alivelist
|
||||
static_cast<GPUUnorderedResource*>(aliveList[0]), // CURRENT alivelist
|
||||
static_cast<GPUUnorderedResource*>(aliveList[1]), // NEW alivelist
|
||||
static_cast<GPUUnorderedResource*>(deadList),
|
||||
static_cast<GPUUnorderedResource*>(counterBuffer),
|
||||
static_cast<GPUUnorderedResource*>(indirectDispatchBuffer),
|
||||
static_cast<GPUUnorderedResource*>(indirectDrawBuffer),
|
||||
static_cast<GPUUnorderedResource*>(indirectBuffers),
|
||||
};
|
||||
device->BindUnorderedAccessResourcesCS(uavs, 0, ARRAYSIZE(uavs), threadID);
|
||||
|
||||
@@ -289,15 +306,15 @@ void wiEmittedParticle::UpdateRenderData(GRAPHICSTHREAD threadID)
|
||||
|
||||
// emit the required amount if there are free slots in dead list
|
||||
device->BindCS(emitCS, threadID);
|
||||
device->DispatchIndirect(indirectDispatchBuffer, 0, threadID);
|
||||
device->DispatchIndirect(indirectBuffers, 0, threadID);
|
||||
|
||||
// kick off simulation based on OLD alivelist count
|
||||
// kick off simulation based on CURRENT alivelist count
|
||||
device->BindCS(simulateargsCS, threadID);
|
||||
device->Dispatch(1, 1, 1, threadID);
|
||||
|
||||
// update OLD alive list, write NEW alive list
|
||||
// update CURRENT alive list, write NEW alive list
|
||||
device->BindCS(simulateCS, threadID);
|
||||
device->DispatchIndirect(indirectDispatchBuffer, 0, threadID);
|
||||
device->DispatchIndirect(indirectBuffers, sizeof(wiGraphicsTypes::IndirectDispatchArgs), threadID);
|
||||
|
||||
// update the draw arguments
|
||||
device->BindCS(drawargsCS, threadID);
|
||||
@@ -310,9 +327,14 @@ void wiEmittedParticle::UpdateRenderData(GRAPHICSTHREAD threadID)
|
||||
|
||||
device->EventEnd(threadID);
|
||||
|
||||
// Swap OLD alivelist with NEW alivelist
|
||||
// Swap CURRENT alivelist with NEW alivelist
|
||||
SwapPtr(aliveList[0], aliveList[1]);
|
||||
emit -= (UINT)emit;
|
||||
|
||||
if (DEBUG)
|
||||
{
|
||||
device->DownloadBuffer(counterBuffer, debugDataReadbackBuffer, &debugData, threadID, false);
|
||||
}
|
||||
}
|
||||
|
||||
void wiEmittedParticle::Draw(GRAPHICSTHREAD threadID)
|
||||
@@ -342,8 +364,7 @@ void wiEmittedParticle::Draw(GRAPHICSTHREAD threadID)
|
||||
device->BindResourcePS(material->texture, TEXSLOT_ONDEMAND0, threadID);
|
||||
}
|
||||
|
||||
//device->Draw((int)points.size() * 6, threadID);
|
||||
device->DrawInstancedIndirect(indirectDrawBuffer, 0, threadID);
|
||||
device->DrawInstancedIndirect(indirectBuffers, sizeof(wiGraphicsTypes::IndirectDispatchArgs) * 2, threadID);
|
||||
|
||||
device->EventEnd(threadID);
|
||||
}
|
||||
@@ -356,9 +377,9 @@ void wiEmittedParticle::CleanUp()
|
||||
SAFE_DELETE(aliveList[1]);
|
||||
SAFE_DELETE(deadList);
|
||||
SAFE_DELETE(counterBuffer);
|
||||
SAFE_DELETE(indirectDispatchBuffer);
|
||||
SAFE_DELETE(indirectDrawBuffer);
|
||||
SAFE_DELETE(indirectBuffers);
|
||||
SAFE_DELETE(constantBuffer);
|
||||
SAFE_DELETE(debugDataReadbackBuffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -527,6 +548,11 @@ void wiEmittedParticle::Serialize(wiArchive& archive)
|
||||
string lightName;
|
||||
archive >> lightName;
|
||||
}
|
||||
if (archive.GetVersion() >= 11)
|
||||
{
|
||||
archive >> MAX_PARTICLES;
|
||||
archive >> SORTING;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
@@ -544,5 +570,10 @@ void wiEmittedParticle::Serialize(wiArchive& archive)
|
||||
archive << scaleY;
|
||||
archive << rotation;
|
||||
archive << motionBlurAmount;
|
||||
if (archive.GetVersion() >= 11)
|
||||
{
|
||||
archive << MAX_PARTICLES;
|
||||
archive << SORTING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@ class wiArchive;
|
||||
class wiEmittedParticle
|
||||
{
|
||||
private:
|
||||
ParticleCounters debugData = {};
|
||||
wiGraphicsTypes::GPUBuffer* debugDataReadbackBuffer;
|
||||
|
||||
wiGraphicsTypes::GPUBuffer* particleBuffer;
|
||||
wiGraphicsTypes::GPUBuffer* aliveList[2];
|
||||
wiGraphicsTypes::GPUBuffer* deadList;
|
||||
wiGraphicsTypes::GPUBuffer* counterBuffer;
|
||||
wiGraphicsTypes::GPUBuffer* indirectDispatchBuffer;
|
||||
wiGraphicsTypes::GPUBuffer* indirectDrawBuffer;
|
||||
wiGraphicsTypes::GPUBuffer* indirectBuffers; // kickoff, simulation, draw
|
||||
wiGraphicsTypes::GPUBuffer* constantBuffer;
|
||||
void CreateSelfBuffers();
|
||||
|
||||
@@ -42,6 +43,7 @@ private:
|
||||
|
||||
float emit;
|
||||
|
||||
bool buffersUpToDate = false;
|
||||
uint32_t MAX_PARTICLES;
|
||||
|
||||
public:
|
||||
@@ -60,6 +62,11 @@ public:
|
||||
void Draw(GRAPHICSTHREAD threadID);
|
||||
void CleanUp();
|
||||
|
||||
bool DEBUG = false;
|
||||
ParticleCounters GetDebugData() { return debugData; }
|
||||
|
||||
bool SORTING = false;
|
||||
|
||||
std::string name;
|
||||
Object* object;
|
||||
std::string materialName;
|
||||
|
||||
+116
-120
@@ -1394,10 +1394,10 @@ void wiRenderer::SetUpStates()
|
||||
bd.AlphaToCoverageEnable = false;
|
||||
GetDevice()->CreateBlendState(&bd, blendStates[BSTYPE_ENVIRONMENTALLIGHT]);
|
||||
|
||||
bd.RenderTarget[0].SrcBlend = BLEND_INV_DEST_COLOR;
|
||||
bd.RenderTarget[0].DestBlend = BLEND_ZERO;
|
||||
bd.RenderTarget[0].SrcBlend = BLEND_INV_SRC_COLOR;
|
||||
bd.RenderTarget[0].DestBlend = BLEND_INV_DEST_COLOR;
|
||||
bd.RenderTarget[0].BlendOp = BLEND_OP_ADD;
|
||||
bd.RenderTarget[0].SrcBlendAlpha = BLEND_ZERO;
|
||||
bd.RenderTarget[0].SrcBlendAlpha = BLEND_ONE;
|
||||
bd.RenderTarget[0].DestBlendAlpha = BLEND_ONE;
|
||||
bd.RenderTarget[0].BlendOpAlpha = BLEND_OP_ADD;
|
||||
bd.RenderTarget[0].BlendEnable = true;
|
||||
@@ -1856,119 +1856,10 @@ void wiRenderer::UpdateRenderData(GRAPHICSTHREAD threadID)
|
||||
UpdateFrameCB(threadID);
|
||||
BindPersistentState(threadID);
|
||||
|
||||
|
||||
wiProfiler::GetInstance().BeginRange("Skinning", wiProfiler::DOMAIN_GPU, threadID);
|
||||
GetDevice()->EventBegin("Skinning", threadID);
|
||||
{
|
||||
bool streamOutSetUp = false;
|
||||
|
||||
for (Model* model : GetScene().models)
|
||||
{
|
||||
// Update material constant buffers:
|
||||
MaterialCB materialGPUData;
|
||||
for (auto& it : model->materials)
|
||||
{
|
||||
Material* material = it.second;
|
||||
materialGPUData.Create(*material);
|
||||
// These will probably not change every time so only issue a GPU memory update if it is necessary:
|
||||
if (memcmp(&material->gpuData, &materialGPUData, sizeof(MaterialCB)) != 0)
|
||||
{
|
||||
material->gpuData = materialGPUData;
|
||||
GetDevice()->UpdateBuffer(&material->constantBuffer, &materialGPUData, threadID);
|
||||
}
|
||||
}
|
||||
|
||||
// Skinning:
|
||||
for (MeshCollection::iterator iter = model->meshes.begin(); iter != model->meshes.end(); ++iter)
|
||||
{
|
||||
Mesh* mesh = iter->second;
|
||||
|
||||
if (mesh->hasArmature() && !mesh->hasDynamicVB() && mesh->renderable && !mesh->vertices_POS.empty()
|
||||
&& mesh->streamoutBuffer_POS.IsValid() && mesh->vertexBuffer_POS.IsValid())
|
||||
{
|
||||
Armature* armature = mesh->armature;
|
||||
|
||||
if (!streamOutSetUp)
|
||||
{
|
||||
// Set up skinning shader
|
||||
streamOutSetUp = true;
|
||||
GetDevice()->BindVertexLayout(nullptr, threadID);
|
||||
GPUBuffer* vbs[] = {
|
||||
nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr
|
||||
};
|
||||
const UINT strides[] = {
|
||||
0,0,0,0,0,0,0,0
|
||||
};
|
||||
GetDevice()->BindVertexBuffers(vbs, 0, ARRAYSIZE(vbs), strides, nullptr, threadID);
|
||||
GetDevice()->BindCS(computeShaders[CSTYPE_SKINNING], threadID);
|
||||
}
|
||||
|
||||
// Upload bones for skinning to shader
|
||||
for (unsigned int k = 0; k < armature->boneCollection.size(); k++)
|
||||
{
|
||||
armature->boneData[k].Create(armature->boneCollection[k]->boneRelativity);
|
||||
}
|
||||
GetDevice()->UpdateBuffer(&armature->boneBuffer, armature->boneData.data(), threadID, (int)(sizeof(Armature::ShaderBoneType) * armature->boneCollection.size()));
|
||||
GetDevice()->BindResourceCS(&armature->boneBuffer, SKINNINGSLOT_IN_BONEBUFFER, threadID);
|
||||
|
||||
// Do the skinning
|
||||
const GPUResource* vbs[] = {
|
||||
static_cast<const GPUResource*>(&mesh->vertexBuffer_POS),
|
||||
static_cast<const GPUResource*>(&mesh->vertexBuffer_NOR),
|
||||
static_cast<const GPUResource*>(&mesh->vertexBuffer_BON),
|
||||
};
|
||||
const GPUUnorderedResource* sos[] = {
|
||||
static_cast<const GPUUnorderedResource*>(&mesh->streamoutBuffer_POS),
|
||||
static_cast<const GPUUnorderedResource*>(&mesh->streamoutBuffer_NOR),
|
||||
static_cast<const GPUUnorderedResource*>(&mesh->streamoutBuffer_PRE),
|
||||
};
|
||||
|
||||
GetDevice()->BindResourcesCS(vbs, SKINNINGSLOT_IN_VERTEX_POS, ARRAYSIZE(vbs), threadID);
|
||||
GetDevice()->BindUnorderedAccessResourcesCS(sos, 0, ARRAYSIZE(sos), threadID);
|
||||
|
||||
GetDevice()->Dispatch((UINT)ceilf((float)mesh->vertices_POS.size() / SKINNING_COMPUTE_THREADCOUNT), 1, 1, threadID);
|
||||
|
||||
}
|
||||
else if (mesh->hasDynamicVB())
|
||||
{
|
||||
// Upload CPU skinned vertex buffer (Soft body VB)
|
||||
mesh->bufferOffset_POS = GetDevice()->AppendRingBuffer(dynamicVertexBufferPool, mesh->vertices_Transformed_POS.data(), sizeof(Mesh::Vertex_POS)*mesh->vertices_Transformed_POS.size(), threadID);
|
||||
mesh->bufferOffset_NOR = GetDevice()->AppendRingBuffer(dynamicVertexBufferPool, mesh->vertices_Transformed_NOR.data(), sizeof(Mesh::Vertex_NOR)*mesh->vertices_Transformed_NOR.size(), threadID);
|
||||
mesh->bufferOffset_PRE = GetDevice()->AppendRingBuffer(dynamicVertexBufferPool, mesh->vertices_Transformed_PRE.data(), sizeof(Mesh::Vertex_POS)*mesh->vertices_Transformed_PRE.size(), threadID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (streamOutSetUp)
|
||||
{
|
||||
// Unload skinning shader
|
||||
GetDevice()->BindCS(nullptr, threadID);
|
||||
GetDevice()->UnBindUnorderedAccessResources(0, 3, threadID);
|
||||
GetDevice()->UnBindResources(SKINNINGSLOT_IN_VERTEX_POS, 4, threadID);
|
||||
}
|
||||
|
||||
}
|
||||
GetDevice()->EventEnd(threadID);
|
||||
wiProfiler::GetInstance().EndRange(threadID); // skinning
|
||||
|
||||
|
||||
RefreshEnvProbes(threadID);
|
||||
|
||||
// Bind environment probes:
|
||||
{
|
||||
const GPUResource* envMaps[] = {
|
||||
globalEnvProbes[0] == nullptr ? enviroMap : globalEnvProbes[0]->cubeMap.GetTexture(),
|
||||
globalEnvProbes[1] == nullptr ? enviroMap : globalEnvProbes[1]->cubeMap.GetTexture(),
|
||||
};
|
||||
GetDevice()->BindResourcesPS(envMaps, TEXSLOT_ENV0, ARRAYSIZE(envMaps), threadID);
|
||||
GetDevice()->BindResourcesCS(envMaps, TEXSLOT_ENV0, ARRAYSIZE(envMaps), threadID);
|
||||
}
|
||||
|
||||
ManageDecalAtlas(threadID);
|
||||
|
||||
const FrameCulling& mainCameraCulling = frameCullings[getCamera()];
|
||||
|
||||
ManageDecalAtlas(threadID);
|
||||
|
||||
// Fill Light Array with lights + decals in the frustum:
|
||||
{
|
||||
const CulledList& culledLights = mainCameraCulling.culledLights;
|
||||
@@ -2125,7 +2016,7 @@ void wiRenderer::UpdateRenderData(GRAPHICSTHREAD threadID)
|
||||
GetDevice()->BindResourcesCS(resources, SBSLOT_ENTITYARRAY, ARRAYSIZE(resources), threadID);
|
||||
}
|
||||
|
||||
// Update visible particlesystem vertices:
|
||||
// Particle system simulation/sorting/culling:
|
||||
for (auto& x : emitterSystems)
|
||||
{
|
||||
x->UpdateRenderData(threadID);
|
||||
@@ -2134,6 +2025,114 @@ void wiRenderer::UpdateRenderData(GRAPHICSTHREAD threadID)
|
||||
{
|
||||
hair->ComputeCulling(getCamera(), threadID);
|
||||
}
|
||||
|
||||
|
||||
wiProfiler::GetInstance().BeginRange("Skinning", wiProfiler::DOMAIN_GPU, threadID);
|
||||
GetDevice()->EventBegin("Skinning", threadID);
|
||||
{
|
||||
bool streamOutSetUp = false;
|
||||
|
||||
for (Model* model : GetScene().models)
|
||||
{
|
||||
// Update material constant buffers:
|
||||
MaterialCB materialGPUData;
|
||||
for (auto& it : model->materials)
|
||||
{
|
||||
Material* material = it.second;
|
||||
materialGPUData.Create(*material);
|
||||
// These will probably not change every time so only issue a GPU memory update if it is necessary:
|
||||
if (memcmp(&material->gpuData, &materialGPUData, sizeof(MaterialCB)) != 0)
|
||||
{
|
||||
material->gpuData = materialGPUData;
|
||||
GetDevice()->UpdateBuffer(&material->constantBuffer, &materialGPUData, threadID);
|
||||
}
|
||||
}
|
||||
|
||||
// Skinning:
|
||||
for (MeshCollection::iterator iter = model->meshes.begin(); iter != model->meshes.end(); ++iter)
|
||||
{
|
||||
Mesh* mesh = iter->second;
|
||||
|
||||
if (mesh->hasArmature() && !mesh->hasDynamicVB() && mesh->renderable && !mesh->vertices_POS.empty()
|
||||
&& mesh->streamoutBuffer_POS.IsValid() && mesh->vertexBuffer_POS.IsValid())
|
||||
{
|
||||
Armature* armature = mesh->armature;
|
||||
|
||||
if (!streamOutSetUp)
|
||||
{
|
||||
// Set up skinning shader
|
||||
streamOutSetUp = true;
|
||||
GetDevice()->BindVertexLayout(nullptr, threadID);
|
||||
GPUBuffer* vbs[] = {
|
||||
nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr
|
||||
};
|
||||
const UINT strides[] = {
|
||||
0,0,0,0,0,0,0,0
|
||||
};
|
||||
GetDevice()->BindVertexBuffers(vbs, 0, ARRAYSIZE(vbs), strides, nullptr, threadID);
|
||||
GetDevice()->BindCS(computeShaders[CSTYPE_SKINNING], threadID);
|
||||
}
|
||||
|
||||
// Upload bones for skinning to shader
|
||||
for (unsigned int k = 0; k < armature->boneCollection.size(); k++)
|
||||
{
|
||||
armature->boneData[k].Create(armature->boneCollection[k]->boneRelativity);
|
||||
}
|
||||
GetDevice()->UpdateBuffer(&armature->boneBuffer, armature->boneData.data(), threadID, (int)(sizeof(Armature::ShaderBoneType) * armature->boneCollection.size()));
|
||||
GetDevice()->BindResourceCS(&armature->boneBuffer, SKINNINGSLOT_IN_BONEBUFFER, threadID);
|
||||
|
||||
// Do the skinning
|
||||
const GPUResource* vbs[] = {
|
||||
static_cast<const GPUResource*>(&mesh->vertexBuffer_POS),
|
||||
static_cast<const GPUResource*>(&mesh->vertexBuffer_NOR),
|
||||
static_cast<const GPUResource*>(&mesh->vertexBuffer_BON),
|
||||
};
|
||||
const GPUUnorderedResource* sos[] = {
|
||||
static_cast<const GPUUnorderedResource*>(&mesh->streamoutBuffer_POS),
|
||||
static_cast<const GPUUnorderedResource*>(&mesh->streamoutBuffer_NOR),
|
||||
static_cast<const GPUUnorderedResource*>(&mesh->streamoutBuffer_PRE),
|
||||
};
|
||||
|
||||
GetDevice()->BindResourcesCS(vbs, SKINNINGSLOT_IN_VERTEX_POS, ARRAYSIZE(vbs), threadID);
|
||||
GetDevice()->BindUnorderedAccessResourcesCS(sos, 0, ARRAYSIZE(sos), threadID);
|
||||
|
||||
GetDevice()->Dispatch((UINT)ceilf((float)mesh->vertices_POS.size() / SKINNING_COMPUTE_THREADCOUNT), 1, 1, threadID);
|
||||
|
||||
}
|
||||
else if (mesh->hasDynamicVB())
|
||||
{
|
||||
// Upload CPU skinned vertex buffer (Soft body VB)
|
||||
mesh->bufferOffset_POS = GetDevice()->AppendRingBuffer(dynamicVertexBufferPool, mesh->vertices_Transformed_POS.data(), sizeof(Mesh::Vertex_POS)*mesh->vertices_Transformed_POS.size(), threadID);
|
||||
mesh->bufferOffset_NOR = GetDevice()->AppendRingBuffer(dynamicVertexBufferPool, mesh->vertices_Transformed_NOR.data(), sizeof(Mesh::Vertex_NOR)*mesh->vertices_Transformed_NOR.size(), threadID);
|
||||
mesh->bufferOffset_PRE = GetDevice()->AppendRingBuffer(dynamicVertexBufferPool, mesh->vertices_Transformed_PRE.data(), sizeof(Mesh::Vertex_POS)*mesh->vertices_Transformed_PRE.size(), threadID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (streamOutSetUp)
|
||||
{
|
||||
// Unload skinning shader
|
||||
GetDevice()->BindCS(nullptr, threadID);
|
||||
GetDevice()->UnBindUnorderedAccessResources(0, 3, threadID);
|
||||
GetDevice()->UnBindResources(SKINNINGSLOT_IN_VERTEX_POS, 4, threadID);
|
||||
}
|
||||
|
||||
}
|
||||
GetDevice()->EventEnd(threadID);
|
||||
wiProfiler::GetInstance().EndRange(threadID); // skinning
|
||||
|
||||
// Render out of date environment probes:
|
||||
RefreshEnvProbes(threadID);
|
||||
|
||||
// Bind environment probes:
|
||||
{
|
||||
const GPUResource* envMaps[] = {
|
||||
globalEnvProbes[0] == nullptr ? enviroMap : globalEnvProbes[0]->cubeMap.GetTexture(),
|
||||
globalEnvProbes[1] == nullptr ? enviroMap : globalEnvProbes[1]->cubeMap.GetTexture(),
|
||||
};
|
||||
GetDevice()->BindResourcesPS(envMaps, TEXSLOT_ENV0, ARRAYSIZE(envMaps), threadID);
|
||||
GetDevice()->BindResourcesCS(envMaps, TEXSLOT_ENV0, ARRAYSIZE(envMaps), threadID);
|
||||
}
|
||||
}
|
||||
void wiRenderer::OcclusionCulling_Render(GRAPHICSTHREAD threadID)
|
||||
{
|
||||
@@ -2765,7 +2764,7 @@ void wiRenderer::DrawDebugEmitters(Camera* camera, GRAPHICSTHREAD threadID)
|
||||
|
||||
GetDevice()->BindRasterizerState(rasterizers[RSTYPE_WIRE_DOUBLESIDED_SMOOTH], threadID);
|
||||
GetDevice()->BindDepthStencilState(depthStencils[DSSTYPE_DEPTHREAD], STENCILREF_EMPTY, threadID);
|
||||
GetDevice()->BindBlendState(blendStates[BSTYPE_INVERSE], threadID);
|
||||
GetDevice()->BindBlendState(blendStates[BSTYPE_OPAQUE], threadID);
|
||||
|
||||
|
||||
GetDevice()->BindPS(pixelShaders[PSTYPE_OBJECT_DEBUG], threadID);
|
||||
@@ -2777,7 +2776,7 @@ void wiRenderer::DrawDebugEmitters(Camera* camera, GRAPHICSTHREAD threadID)
|
||||
if (x->object != nullptr && x->object->mesh != nullptr)
|
||||
{
|
||||
sb.mTransform = XMMatrixTranspose(XMLoadFloat4x4(&x->object->world)*camera->GetViewProjection());
|
||||
sb.mColor = XMFLOAT4(1, 1, 1, 1);
|
||||
sb.mColor = XMFLOAT4(0, 1, 0, 1);
|
||||
GetDevice()->UpdateBuffer(constantBuffers[CBTYPE_MISC], &sb, threadID);
|
||||
|
||||
const GPUBuffer* vbs[] = {
|
||||
@@ -2789,10 +2788,7 @@ void wiRenderer::DrawDebugEmitters(Camera* camera, GRAPHICSTHREAD threadID)
|
||||
GetDevice()->BindVertexBuffers(vbs, 0, ARRAYSIZE(vbs), strides, nullptr, threadID);
|
||||
GetDevice()->BindIndexBuffer(&x->object->mesh->indexBuffer, x->object->mesh->GetIndexFormat(), 0, threadID);
|
||||
|
||||
for (auto& y : x->object->mesh->subsets)
|
||||
{
|
||||
GetDevice()->DrawIndexed((int)y.subsetIndices.size(), y.indexBufferOffset, 0, threadID);
|
||||
}
|
||||
GetDevice()->DrawIndexed((int)x->object->mesh->indices.size(), 0, 0, threadID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace wiVersion
|
||||
// minor features, major updates
|
||||
const int minor = 13;
|
||||
// minor bug fixes, alterations, refactors, updates
|
||||
const int revision = 70;
|
||||
const int revision = 71;
|
||||
|
||||
|
||||
long GetVersion()
|
||||
|
||||
Reference in New Issue
Block a user