diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5ab3a103..74f831eef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,13 +21,14 @@ TombEngine releases are located in this repository (alongside with Tomb Editor):
* Fixed custom shatter sounds with custom sound IDs not playing correctly.
### New Features
-* Added diary module.
+* Added multithreading and an option for it to flow system settings.
* Added ability to use keys and puzzle items underwater.
- You must update your Lara object: https://github.com/TombEngine/Resources/raw/main/Wad2%20Objects/Lara/TEN_Lara.wad2
* Added a particle based waterfall emitter object and associated sprite slots.
- You must use this version: https://github.com/TombEngine/Resources/raw/refs/heads/main/Wad2%20Objects/Interactables/TEN_Waterfall_Emitter.wad2
### Lua API changes
+* Added diary module.
* Added Effects.EmitAirBubble() function to spawn air bubbles.
* Added additional arguments for Sprite object slot and starting rotation value for EmitParticle function.
* Added various Translate() methods to Vec2 and Vec3 script objects.
diff --git a/Documentation/doc/2 classes/Flow.Settings.html b/Documentation/doc/2 classes/Flow.Settings.html
index 229e91651..fefca5dfe 100644
--- a/Documentation/doc/2 classes/Flow.Settings.html
+++ b/Documentation/doc/2 classes/Flow.Settings.html
@@ -305,6 +305,10 @@
How should the application respond to script errors? |
+ multiThreaded |
+ Use multithreading in certain calculations. |
+
+
fastReload |
Can the game utilize the fast reload feature? |
@@ -1253,6 +1257,29 @@
+
+
+
+ multiThreaded
+
+
+ Use multithreading in certain calculations.
+ When set to true
, some performance-critical calculations will be performed in parallel, which can give
+ a significant performance boost. Don't disable unless you have problems with launching or using TombEngine.
+
+
+
+
+ - multiThreaded
+ bool
+ determines whether to use multithreading or not. */
+
+
+
+
+
+
+
diff --git a/TombEngine/Game/Setup.cpp b/TombEngine/Game/Setup.cpp
index 84bd677d6..733989cb6 100644
--- a/TombEngine/Game/Setup.cpp
+++ b/TombEngine/Game/Setup.cpp
@@ -78,38 +78,38 @@ ObjectInfo& ObjectHandler::GetFirstAvailableObject()
void StaticHandler::Initialize()
{
- _lookupTable.resize(0);
- _lookupTable.reserve(_defaultLUTSize);
+ _lut.resize(0);
+ _lut.reserve(LUT_SIZE);
_statics.resize(0);
}
int StaticHandler::GetIndex(int staticID)
{
- if (staticID < 0 || staticID >= _lookupTable.size())
+ if (staticID < 0 || staticID >= _lut.size())
{
- TENLog("Attempt to get nonexistent static mesh ID slot index (" + std::to_string(staticID) + ")", LogLevel::Warning);
- return _lookupTable.front();
+ TENLog("Attempted to get index of missing static object " + std::to_string(staticID) + ".", LogLevel::Warning);
+ return _lut.front();
}
- return _lookupTable[staticID];
+ return _lut[staticID];
}
StaticInfo& StaticHandler::operator [](int staticID)
{
if (staticID < 0)
{
- TENLog("Attempt to access illegal static mesh ID slot info", LogLevel::Warning);
+ TENLog("Attempted to access missing static object " + std::to_string(staticID) + ".", LogLevel::Warning);
return _statics.front();
}
- if (staticID >= _lookupTable.size())
- _lookupTable.resize(staticID + 1, NO_VALUE);
+ if (staticID >= _lut.size())
+ _lut.resize(staticID + 1, NO_VALUE);
- if (_lookupTable[staticID] != NO_VALUE)
- return _statics[_lookupTable[staticID]];
+ if (_lut[staticID] != NO_VALUE)
+ return _statics[_lut[staticID]];
_statics.emplace_back();
- _lookupTable[staticID] = (int)_statics.size() - 1;
+ _lut[staticID] = (int)_statics.size() - 1;
return _statics.back();
}
diff --git a/TombEngine/Game/Setup.h b/TombEngine/Game/Setup.h
index 2ba3a4796..0cee4c098 100644
--- a/TombEngine/Game/Setup.h
+++ b/TombEngine/Game/Setup.h
@@ -135,10 +135,10 @@ struct StaticInfo
class StaticHandler
{
private:
- static const int _defaultLUTSize = 256;
+ static constexpr auto LUT_SIZE = 256;
std::vector _statics = {};
- std::vector _lookupTable = {};
+ std::vector _lut = {};
public:
void Initialize();
@@ -147,6 +147,7 @@ public:
StaticInfo& operator [](int staticID);
// Iterators
+
auto begin() { return _statics.begin(); } // Non-const begin
auto end() { return _statics.end(); } // Non-const end
auto begin() const { return _statics.cbegin(); } // Const begin
diff --git a/TombEngine/Renderer/Renderer.h b/TombEngine/Renderer/Renderer.h
index 68f5fc9a5..db0f7e115 100644
--- a/TombEngine/Renderer/Renderer.h
+++ b/TombEngine/Renderer/Renderer.h
@@ -509,7 +509,7 @@ namespace TEN::Renderer
const Vector4& color0, const Vector4& color1, const Vector4& color2, const Vector4& color3,
BlendMode blendMode, RenderView& view, SpriteRenderType renderType = SpriteRenderType::Default);
- Matrix GetWorldMatrixForSprite(RendererSpriteToDraw* spr, RenderView& view);
+ Matrix GetWorldMatrixForSprite(const RendererSpriteToDraw& sprite, RenderView& view);
RendererObject& GetRendererObject(GAME_OBJECT_ID id);
RendererMesh* GetMesh(int meshIndex);
Texture2D CreateDefaultNormalTexture();
diff --git a/TombEngine/Renderer/RendererDraw.cpp b/TombEngine/Renderer/RendererDraw.cpp
index ab1606014..49dbeb089 100644
--- a/TombEngine/Renderer/RendererDraw.cpp
+++ b/TombEngine/Renderer/RendererDraw.cpp
@@ -2925,7 +2925,7 @@ namespace TEN::Renderer
rDrawSprite.Width = STAR_SIZE * star.Scale;
rDrawSprite.Height = STAR_SIZE * star.Scale;
- _stInstancedSpriteBuffer.Sprites[i].World = GetWorldMatrixForSprite(&rDrawSprite, renderView);
+ _stInstancedSpriteBuffer.Sprites[i].World = GetWorldMatrixForSprite(rDrawSprite, renderView);
_stInstancedSpriteBuffer.Sprites[i].Color = Vector4(
star.Color.x,
star.Color.y,
@@ -2988,7 +2988,7 @@ namespace TEN::Renderer
rDrawSprite.Height = 192;
rDrawSprite.ConstrainAxis = meteor.Direction;
- _stInstancedSpriteBuffer.Sprites[i].World = GetWorldMatrixForSprite(&rDrawSprite, renderView);
+ _stInstancedSpriteBuffer.Sprites[i].World = GetWorldMatrixForSprite(rDrawSprite, renderView);
_stInstancedSpriteBuffer.Sprites[i].Color = Vector4(
meteor.Color.x,
meteor.Color.y,
@@ -3092,7 +3092,7 @@ namespace TEN::Renderer
rDrawSprite.Height = SUN_SIZE;
rDrawSprite.color = renderView.LensFlaresToDraw[0].Color;
- _stInstancedSpriteBuffer.Sprites[0].World = GetWorldMatrixForSprite(&rDrawSprite, renderView);
+ _stInstancedSpriteBuffer.Sprites[0].World = GetWorldMatrixForSprite(rDrawSprite, renderView);
_stInstancedSpriteBuffer.Sprites[0].Color = renderView.LensFlaresToDraw[0].Color;
_stInstancedSpriteBuffer.Sprites[0].IsBillboard = 1;
_stInstancedSpriteBuffer.Sprites[0].IsSoftParticle = 0;
@@ -3442,7 +3442,7 @@ namespace TEN::Renderer
uv2 = spr->Sprite->UV[2];
uv3 = spr->Sprite->UV[3];
- auto world = GetWorldMatrixForSprite(currentObject->Sprite, view);
+ auto world = GetWorldMatrixForSprite(*currentObject->Sprite, view);
Vertex v0;
v0.Position = Vector3::Transform(p0t, world);
diff --git a/TombEngine/Renderer/RendererDrawEffect.cpp b/TombEngine/Renderer/RendererDrawEffect.cpp
index 14076756c..988e0b186 100644
--- a/TombEngine/Renderer/RendererDrawEffect.cpp
+++ b/TombEngine/Renderer/RendererDrawEffect.cpp
@@ -1291,14 +1291,14 @@ namespace TEN::Renderer
}
}
- Matrix Renderer::GetWorldMatrixForSprite(RendererSpriteToDraw* sprite, RenderView& view)
+ Matrix Renderer::GetWorldMatrixForSprite(const RendererSpriteToDraw& sprite, RenderView& view)
{
auto spriteMatrix = Matrix::Identity;
- auto scaleMatrix = Matrix::CreateScale(sprite->Width * sprite->Scale, sprite->Height * sprite->Scale, sprite->Scale);
+ auto scaleMatrix = Matrix::CreateScale(sprite.Width * sprite.Scale, sprite.Height * sprite.Scale, sprite.Scale);
- auto spritePos = sprite->pos;
+ auto spritePos = sprite.pos;
- if (sprite->Type == SpriteType::ThreeD)
+ if (sprite.Type == SpriteType::ThreeD)
{
ReflectMatrixOptionally(spriteMatrix);
}
@@ -1307,23 +1307,23 @@ namespace TEN::Renderer
ReflectVectorOptionally(spritePos);
}
- switch (sprite->Type)
+ switch (sprite.Type)
{
case SpriteType::Billboard:
{
auto cameraUp = Vector3(view.Camera.View._12, view.Camera.View._22, view.Camera.View._32);
- spriteMatrix = scaleMatrix * Matrix::CreateRotationZ(sprite->Rotation) * Matrix::CreateBillboard(spritePos, Camera.pos.ToVector3(), cameraUp);
+ spriteMatrix = scaleMatrix * Matrix::CreateRotationZ(sprite.Rotation) * Matrix::CreateBillboard(spritePos, Camera.pos.ToVector3(), cameraUp);
}
break;
case SpriteType::CustomBillboard:
{
- auto rotMatrix = Matrix::CreateRotationY(sprite->Rotation);
+ auto rotMatrix = Matrix::CreateRotationY(sprite.Rotation);
auto quadForward = Vector3(0.0f, 0.0f, 1.0f);
spriteMatrix = scaleMatrix * Matrix::CreateConstrainedBillboard(
spritePos,
Camera.pos.ToVector3(),
- sprite->ConstrainAxis,
+ sprite.ConstrainAxis,
nullptr,
&quadForward);
}
@@ -1332,7 +1332,7 @@ namespace TEN::Renderer
case SpriteType::LookAtBillboard:
{
auto translationMatrix = Matrix::CreateTranslation(spritePos);
- auto rotMatrix = Matrix::CreateRotationZ(sprite->Rotation) * Matrix::CreateLookAt(Vector3::Zero, sprite->LookAtAxis, Vector3::UnitZ);
+ auto rotMatrix = Matrix::CreateRotationZ(sprite.Rotation) * Matrix::CreateLookAt(Vector3::Zero, sprite.LookAtAxis, Vector3::UnitZ);
spriteMatrix = scaleMatrix * rotMatrix * translationMatrix;
}
break;
diff --git a/TombEngine/Renderer/RendererSprites.cpp b/TombEngine/Renderer/RendererSprites.cpp
index 22e841990..03a0c67bd 100644
--- a/TombEngine/Renderer/RendererSprites.cpp
+++ b/TombEngine/Renderer/RendererSprites.cpp
@@ -1,14 +1,16 @@
#include "framework.h"
#include "Renderer/Structures/RendererSprite.h"
+
#include "Renderer/Structures/RendererSpriteBucket.h"
#include "Renderer/Renderer.h"
+#include "Specific/Parallel.h"
+
+using namespace TEN::Renderer::Structures;
namespace TEN::Renderer
{
- using namespace TEN::Renderer::Structures;
-
void Renderer::AddSpriteBillboard(RendererSprite* sprite, const Vector3& pos, const Vector4& color, float orient2D, float scale,
- Vector2 size, BlendMode blendMode, bool isSoftParticle, RenderView& view, SpriteRenderType renderType)
+ Vector2 size, BlendMode blendMode, bool isSoftParticle, RenderView& view, SpriteRenderType renderType)
{
if (scale <= 0.0f)
scale = 1.0f;
@@ -38,8 +40,8 @@ namespace TEN::Renderer
}
void Renderer::AddSpriteBillboardConstrained(RendererSprite* sprite, const Vector3& pos, const Vector4& color, float orient2D,
- float scale, Vector2 size, BlendMode blendMode, const Vector3& constrainAxis,
- bool isSoftParticle, RenderView& view, SpriteRenderType renderType)
+ float scale, Vector2 size, BlendMode blendMode, const Vector3& constrainAxis,
+ bool isSoftParticle, RenderView& view, SpriteRenderType renderType)
{
if (scale <= 0.0f)
scale = 1.0f;
@@ -261,16 +263,16 @@ namespace TEN::Renderer
return;
// Draw instanced sprites.
- bool wasGPUSet = false;
- for (auto& spriteBucket : _spriteBuckets)
+ bool wasGpuSet = false;
+ for (const auto& spriteBucket : _spriteBuckets)
{
- if (spriteBucket.SpritesToDraw.size() == 0 || !spriteBucket.IsBillboard)
+ if (spriteBucket.SpritesToDraw.empty() || !spriteBucket.IsBillboard)
continue;
if (!SetupBlendModeAndAlphaTest(spriteBucket.BlendMode, rendererPass, 0))
continue;
- if (!wasGPUSet)
+ if (!wasGpuSet)
{
_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
@@ -286,42 +288,45 @@ namespace TEN::Renderer
unsigned int offset = 0;
_context->IASetVertexBuffers(0, 1, _quadVertexBuffer.Buffer.GetAddressOf(), &stride, &offset);
- wasGPUSet = true;
+ wasGpuSet = true;
}
- // Prepare constant buffer for instanced sprites.
- for (int i = 0; i < spriteBucket.SpritesToDraw.size(); i++)
+ // Define sprite preparation logic.
+ auto prepareSprites = [&](int start, int end)
{
- auto& rDrawSprite = spriteBucket.SpritesToDraw[i];
+ for (int i = start; i < end; i++)
+ {
+ const auto& spriteToDraw = spriteBucket.SpritesToDraw[i];
- _stInstancedSpriteBuffer.Sprites[i].World = GetWorldMatrixForSprite(&rDrawSprite, view);
- _stInstancedSpriteBuffer.Sprites[i].Color = rDrawSprite.color;
- _stInstancedSpriteBuffer.Sprites[i].IsBillboard = 1;
- _stInstancedSpriteBuffer.Sprites[i].IsSoftParticle = rDrawSprite.SoftParticle ? 1 : 0;
+ _stInstancedSpriteBuffer.Sprites[i].World = GetWorldMatrixForSprite(spriteToDraw, view);
+ _stInstancedSpriteBuffer.Sprites[i].Color = spriteToDraw.color;
+ _stInstancedSpriteBuffer.Sprites[i].IsBillboard = 1.0f;
+ _stInstancedSpriteBuffer.Sprites[i].IsSoftParticle = spriteToDraw.SoftParticle ? 1.0f : 0.0f;
- // NOTE: Strange packing due to particular HLSL 16 byte alignment requirements.
- _stInstancedSpriteBuffer.Sprites[i].UV[0].x = rDrawSprite.Sprite->UV[0].x;
- _stInstancedSpriteBuffer.Sprites[i].UV[0].y = rDrawSprite.Sprite->UV[1].x;
- _stInstancedSpriteBuffer.Sprites[i].UV[0].z = rDrawSprite.Sprite->UV[2].x;
- _stInstancedSpriteBuffer.Sprites[i].UV[0].w = rDrawSprite.Sprite->UV[3].x;
- _stInstancedSpriteBuffer.Sprites[i].UV[1].x = rDrawSprite.Sprite->UV[0].y;
- _stInstancedSpriteBuffer.Sprites[i].UV[1].y = rDrawSprite.Sprite->UV[1].y;
- _stInstancedSpriteBuffer.Sprites[i].UV[1].z = rDrawSprite.Sprite->UV[2].y;
- _stInstancedSpriteBuffer.Sprites[i].UV[1].w = rDrawSprite.Sprite->UV[3].y;
- }
+ // NOTE: Strange packing due to particular HLSL 16 byte alignment requirements.
+ _stInstancedSpriteBuffer.Sprites[i].UV[0].x = spriteToDraw.Sprite->UV[0].x;
+ _stInstancedSpriteBuffer.Sprites[i].UV[0].y = spriteToDraw.Sprite->UV[1].x;
+ _stInstancedSpriteBuffer.Sprites[i].UV[0].z = spriteToDraw.Sprite->UV[2].x;
+ _stInstancedSpriteBuffer.Sprites[i].UV[0].w = spriteToDraw.Sprite->UV[3].x;
+ _stInstancedSpriteBuffer.Sprites[i].UV[1].x = spriteToDraw.Sprite->UV[0].y;
+ _stInstancedSpriteBuffer.Sprites[i].UV[1].y = spriteToDraw.Sprite->UV[1].y;
+ _stInstancedSpriteBuffer.Sprites[i].UV[1].z = spriteToDraw.Sprite->UV[2].y;
+ _stInstancedSpriteBuffer.Sprites[i].UV[1].w = spriteToDraw.Sprite->UV[3].y;
+ }
+ };
+ g_Parallel.AddTasks((int)spriteBucket.SpritesToDraw.size(), prepareSprites).wait();
BindTexture(TextureRegister::ColorMap, spriteBucket.Sprite->Texture, SamplerStateRegister::LinearClamp);
-
_cbInstancedSpriteBuffer.UpdateData(_stInstancedSpriteBuffer, _context.Get());
// Draw sprites with instancing.
- DrawInstancedTriangles(4, (unsigned int)spriteBucket.SpritesToDraw.size(), 0);
+ DrawInstancedTriangles(4, (int)spriteBucket.SpritesToDraw.size(), 0);
_numInstancedSpritesDrawCalls++;
}
// Draw 3D non-instanced sprites.
- wasGPUSet = false;
+ wasGpuSet = false;
for (auto& spriteBucket : _spriteBuckets)
{
@@ -331,7 +336,7 @@ namespace TEN::Renderer
if (!SetupBlendModeAndAlphaTest(spriteBucket.BlendMode, rendererPass, 0))
continue;
- if (!wasGPUSet)
+ if (!wasGpuSet)
{
_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
@@ -342,10 +347,10 @@ namespace TEN::Renderer
_shaders.Bind(Shader::Sprites);
- wasGPUSet = true;
+ wasGpuSet = true;
}
- _stSprite.IsSoftParticle = spriteBucket.IsSoftParticle ? 1 : 0;
+ _stSprite.IsSoftParticle = spriteBucket.IsSoftParticle ? 1.0f : 0.0f;
_stSprite.RenderType = (int)spriteBucket.RenderType;
_cbSprite.UpdateData(_stSprite, _context.Get());
@@ -414,11 +419,11 @@ namespace TEN::Renderer
_shaders.Bind(Shader::InstancedSprites);
// Set up vertex buffer and parameters.
- UINT stride = sizeof(Vertex);
- UINT offset = 0;
+ unsigned int stride = sizeof(Vertex);
+ unsigned int offset = 0;
_context->IASetVertexBuffers(0, 1, _quadVertexBuffer.Buffer.GetAddressOf(), &stride, &offset);
- _stInstancedSpriteBuffer.Sprites[0].World = GetWorldMatrixForSprite(object->Sprite, view);
+ _stInstancedSpriteBuffer.Sprites[0].World = GetWorldMatrixForSprite(*object->Sprite, view);
_stInstancedSpriteBuffer.Sprites[0].Color = object->Sprite->color;
_stInstancedSpriteBuffer.Sprites[0].IsBillboard = 1;
_stInstancedSpriteBuffer.Sprites[0].IsSoftParticle = object->Sprite->SoftParticle ? 1 : 0;
diff --git a/TombEngine/Scripting/Internal/TEN/Flow/Settings/Settings.cpp b/TombEngine/Scripting/Internal/TEN/Flow/Settings/Settings.cpp
index 6c5092676..ba2cf5e71 100644
--- a/TombEngine/Scripting/Internal/TEN/Flow/Settings/Settings.cpp
+++ b/TombEngine/Scripting/Internal/TEN/Flow/Settings/Settings.cpp
@@ -328,6 +328,12 @@ namespace TEN::Scripting
// @tfield Flow.ErrorMode errorMode error mode to use. */
"errorMode", &SystemSettings::ErrorMode,
+ /// Use multithreading in certain calculations.
+ // When set to `true`, some performance-critical calculations will be performed in parallel, which can give
+ // a significant performance boost. Don't disable unless you have problems with launching or using TombEngine.
+ // @tfield bool multithreaded determines whether to use multithreading or not. */
+ "multithreaded", &SystemSettings::Multithreaded,
+
/// Can the game utilize the fast reload feature?
// When set to `true`, the game will attempt to perform fast savegame reloading if current level is the same as
// the level loaded from the savegame. It will not work if the level timestamp or checksum has changed
diff --git a/TombEngine/Scripting/Internal/TEN/Flow/Settings/Settings.h b/TombEngine/Scripting/Internal/TEN/Flow/Settings/Settings.h
index 9ab2ac617..7b2d962ad 100644
--- a/TombEngine/Scripting/Internal/TEN/Flow/Settings/Settings.h
+++ b/TombEngine/Scripting/Internal/TEN/Flow/Settings/Settings.h
@@ -78,8 +78,9 @@ namespace TEN::Scripting
struct SystemSettings
{
- ErrorMode ErrorMode = ErrorMode::Warn;
- bool FastReload = true;
+ ErrorMode ErrorMode = ErrorMode::Warn;
+ bool FastReload = true;
+ bool Multithreaded = true;
static void Register(sol::table& parent);
};
diff --git a/TombEngine/Specific/Parallel.cpp b/TombEngine/Specific/Parallel.cpp
new file mode 100644
index 000000000..ac7524cc0
--- /dev/null
+++ b/TombEngine/Specific/Parallel.cpp
@@ -0,0 +1,173 @@
+#include "framework.h"
+#include "Specific/Parallel.h"
+
+#include "Scripting/Include/Flow/ScriptInterfaceFlowHandler.h"
+
+namespace TEN::Utils
+{
+ ParallelTaskManager& g_Parallel = ParallelTaskManager::Get();
+
+ ParallelTaskManager::ParallelTaskManager()
+ {
+ _deinitialize = false;
+ }
+
+ ParallelTaskManager::~ParallelTaskManager()
+ {
+ // LOCK: Restrict shutdown flag access.
+ {
+ auto taskLock = std::lock_guard(_taskMutex);
+
+ _deinitialize = true;
+ }
+
+ // Notify all threads they should stop.
+ _taskCond.notify_all();
+
+ // Join all threads.
+ for (auto& thread : _threads)
+ {
+ if (!thread.joinable())
+ continue;
+
+ try
+ {
+ thread.join();
+ }
+ catch (const std::exception& ex)
+ {
+ TENLog("Error joining thread: " + std::string(ex.what()), LogLevel::Error);
+ }
+ }
+ }
+
+ ParallelTaskManager& ParallelTaskManager::Get()
+ {
+ static auto instance = ParallelTaskManager();
+ return instance;
+ }
+
+ unsigned int ParallelTaskManager::GetThreadCount() const
+ {
+ return (unsigned int)_threads.size();
+ }
+
+ unsigned int ParallelTaskManager::GetCoreCount() const
+ {
+ return std::max(std::thread::hardware_concurrency(), 1u);
+ }
+
+ void ParallelTaskManager::Initialize()
+ {
+ // Reserve threads.
+ unsigned int threadCount = g_GameFlow->GetSettings()->System.Multithreaded ? (GetCoreCount() * 2) : 1;
+ _threads.reserve(threadCount);
+
+ // Create threads.
+ for (int i = 0; i < threadCount; i++)
+ _threads.push_back(std::thread(&ParallelTaskManager::Worker, this));
+ }
+
+ std::future ParallelTaskManager::AddTask(const ParallelTask& task)
+ {
+ return AddTasks(ParallelTaskGroup{ task });
+ }
+
+ std::future ParallelTaskManager::AddTasks(const ParallelTaskGroup& tasks)
+ {
+ // HEAP ALLOC: Create counter and promise.
+ auto counter = std::make_shared>();
+ auto promise = std::make_shared>();
+
+ counter->store((int)tasks.size(), std::memory_order_release);
+
+ // Add group tasks.
+ for (const auto& task : tasks)
+ AddTask(task, counter, promise);
+
+ // Notify available threads to handle tasks.
+ _taskCond.notify_all();
+
+ // Return future to wait on task group completion if needed.
+ return promise->get_future();
+ }
+
+ std::future ParallelTaskManager::AddTasks(int elementCount, const std::function& splitTask)
+ {
+ // TODO: Make this a configuration option?
+ constexpr auto SERIAL_UNIT_COUNT_MAX = 32;
+
+ auto tasks = ParallelTaskGroup{};
+
+ // Process in parallel.
+ if (g_GameFlow->GetSettings()->System.Multithreaded &&
+ elementCount > SERIAL_UNIT_COUNT_MAX)
+ {
+ int threadCount = GetCoreCount();
+ int chunkSize = ((elementCount + threadCount) - 1) / threadCount;
+
+ // Collect group tasks.
+ tasks.reserve(threadCount);
+ for (int i = 0; i < threadCount; i++)
+ {
+ int start = i * chunkSize;
+ int end = std::min(start + chunkSize, elementCount);
+ tasks.push_back([&splitTask, start, end]() { splitTask(start, end); });
+ }
+ }
+ // Process linearly.
+ else
+ {
+ tasks.push_back([&splitTask, elementCount]() { splitTask(0, elementCount); });
+ }
+
+ // Add task group and return future to wait on completion if needed.
+ return AddTasks(tasks);
+ }
+
+ void ParallelTaskManager::Worker()
+ {
+ while (true)
+ {
+ auto task = ParallelTask();
+
+ // LOCK: Restrict task queue access.
+ {
+ auto taskLock = std::unique_lock(_taskMutex);
+ _taskCond.wait(taskLock, [this] { return (_deinitialize || !_tasks.empty()); });
+
+ // Shutting down and no pending tasks; return early.
+ if (_deinitialize && _tasks.empty())
+ return;
+
+ // Get task.
+ task = _tasks.front();
+ _tasks.pop();
+ }
+
+ // Execute task.
+ if (task)
+ task();
+ }
+ }
+
+ void ParallelTaskManager::AddTask(const ParallelTask& task, std::shared_ptr> counter, std::shared_ptr> promise)
+ {
+ // Increment counter for task group.
+ counter->fetch_add(1, std::memory_order_relaxed);
+
+ // Add task with promise and counter handling.
+ _tasks.push([this, task, counter, promise]() { HandleTask(task, *counter, *promise); });
+ }
+
+ void ParallelTaskManager::HandleTask(const ParallelTask& task, std::atomic& counter, std::promise& promise)
+ {
+ // Execute task.
+ if (task)
+ task();
+
+ // Check for task group completion.
+ if (counter.fetch_sub(1, std::memory_order_acq_rel) == 1)
+ promise.set_value();
+ }
+}
diff --git a/TombEngine/Specific/Parallel.h b/TombEngine/Specific/Parallel.h
new file mode 100644
index 000000000..1115c21da
--- /dev/null
+++ b/TombEngine/Specific/Parallel.h
@@ -0,0 +1,54 @@
+#pragma once
+
+namespace TEN::Utils
+{
+ using ParallelTask = std::function;
+ using ParallelTaskGroup = std::vector;
+
+ class ParallelTaskManager
+ {
+ private:
+ // Fields
+
+ std::vector _threads = {};
+ std::queue _tasks = {};
+ std::mutex _taskMutex = {};
+ std::condition_variable _taskCond = {};
+ bool _deinitialize = false;
+
+ // Constructors, destructors
+
+ ParallelTaskManager();
+ ParallelTaskManager(const ParallelTaskManager& manager) = delete;
+ ~ParallelTaskManager();
+
+ public:
+ // Getters
+
+ static ParallelTaskManager& Get();
+
+ unsigned int GetThreadCount() const;
+ unsigned int GetCoreCount() const;
+
+ // Utilities
+
+ void Initialize();
+
+ std::future AddTask(const ParallelTask& task);
+ std::future AddTasks(const ParallelTaskGroup& tasks);
+ std::future AddTasks(int elementCount, const std::function& splitTask);
+
+ private:
+ // Helpers
+
+ void Worker();
+ void AddTask(const ParallelTask& task, std::shared_ptr> counter, std::shared_ptr> promise);
+ void HandleTask(const ParallelTask& task, std::atomic& counter, std::promise& promise);
+
+ // Operators
+
+ ParallelTaskManager& operator =(const ParallelTaskManager& manager) = delete;
+ };
+
+ extern ParallelTaskManager& g_Parallel;
+}
diff --git a/TombEngine/Specific/winmain.cpp b/TombEngine/Specific/winmain.cpp
index 789fa8eb0..bd7b2cefa 100644
--- a/TombEngine/Specific/winmain.cpp
+++ b/TombEngine/Specific/winmain.cpp
@@ -14,6 +14,7 @@
#include "Sound/sound.h"
#include "Specific/level.h"
#include "Specific/configuration.h"
+#include "Specific/Parallel.h"
#include "Specific/trutils.h"
#include "Scripting/Internal/LanguageScript.h"
#include "Scripting/Include/ScriptInterfaceState.h"
@@ -592,6 +593,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
DoTheGame = true;
+ g_Parallel.Initialize();
ThreadEnded = false;
ThreadHandle = BeginThread(GameMain, ThreadID);
diff --git a/TombEngine/TombEngine.vcxproj b/TombEngine/TombEngine.vcxproj
index 00bbc1afa..1587af359 100644
--- a/TombEngine/TombEngine.vcxproj
+++ b/TombEngine/TombEngine.vcxproj
@@ -860,6 +860,7 @@ xcopy /Y "$(SolutionDir)Libs\zlib\x64\*.dll" "$(TargetDir)"
+
@@ -1290,6 +1291,7 @@ xcopy /Y "$(SolutionDir)Libs\zlib\x64\*.dll" "$(TargetDir)"
+
diff --git a/TombEngine/framework.h b/TombEngine/framework.h
index 8b57fc862..eac52766c 100644
--- a/TombEngine/framework.h
+++ b/TombEngine/framework.h
@@ -2,6 +2,7 @@
#include
#include
+#include
#include
#include
#include
@@ -20,6 +21,7 @@
#include
#include
#include
+#include
#include
using namespace DirectX;