Files
WickedEngine/WickedEngine/wiBacklog.cpp
T
2026-01-11 21:53:46 +01:00

738 lines
17 KiB
C++

#include "wiBacklog.h"
#include "wiMath.h"
#include "wiResourceManager.h"
#include "wiTextureHelper.h"
#include "wiFont.h"
#include "wiSpriteFont.h"
#include "wiImage.h"
#include "wiLua.h"
#include "wiInput.h"
#include "wiPlatform.h"
#include "wiHelper.h"
#include "wiGUI.h"
#include "wiTimer.h"
#include <mutex>
#include <deque>
#include <limits>
#include <iostream>
#include <thread>
#include <condition_variable>
#include <atomic>
#include <fstream>
#include <chrono>
using namespace wi::graphics;
namespace wi::backlog
{
bool enabled = false;
bool was_ever_enabled = enabled;
const float speed = 4000.0f;
const size_t deleteFromLine = 500; // Only limits in-memory display buffer, not file
float pos = 5;
float scroll = 0;
int historyPos = 0;
wi::font::Params font_params;
wi::Color backgroundColor = wi::Color(17, 30, 43, 255);
Texture backgroundTex;
bool refitscroll = false;
wi::gui::TextInputField inputField;
wi::gui::Button toggleButton;
wi::gui::GUI GUI;
bool locked = false;
bool blockLuaExec = false;
LogLevel logLevel = LogLevel::Default;
LogLevel unseen = LogLevel::None;
std::deque<LogEntry> history;
std::mutex historyLock;
std::string logfile_path = "";
struct AsyncWriter
{
std::thread writerThread;
std::mutex queueMutex;
std::condition_variable queueEntriesCondition;
std::condition_variable queueEmptyCondition;
std::deque<std::string> writeQueue;
std::atomic<bool> running{ false };
std::atomic<bool> initialized{ false };
std::atomic<uint32_t> autoFlushInterval{ 1000 };
wi::Timer lastFlushTimer;
std::ofstream logFileStream;
std::string currentLogFilePath;
std::mutex flushMutex; // Synchronous flush requests
void Start(const std::string& filepath)
{
if (initialized.exchange(true))
{
return;
}
currentLogFilePath = filepath;
running = true;
lastFlushTimer.record();
logFileStream.open(filepath, std::ios::binary | std::ios::trunc);
if (!logFileStream.is_open())
{
wi::helper::DebugOut("Failed to open log file: " + filepath + "\n", wi::helper::DebugLevel::Error);
}
writerThread = std::thread([this]() {
WriterLoop();
});
}
void Stop()
{
if (!initialized.load())
{
return;
}
running = false;
queueEntriesCondition.notify_all();
if (writerThread.joinable())
{
writerThread.join();
}
// Final flush of any remaining entries
FlushQueue();
if (logFileStream.is_open())
{
logFileStream.flush();
logFileStream.close();
}
initialized = false;
}
void WriterLoop()
{
while (running.load())
{
std::unique_lock lock(queueMutex);
// Wait for new entries or timeout for periodic flush
auto timeout = std::chrono::milliseconds(100);
queueEntriesCondition.wait_for(lock, timeout, [this]() {
return !writeQueue.empty() || !running.load();
});
if (!running.load() && writeQueue.empty())
break;
// Process all queued entries
static std::deque<std::string> localQueue;
std::swap(localQueue, writeQueue);
lock.unlock();
// Notify waiters that queue is now empty after swap
queueEmptyCondition.notify_all();
WriteEntries(localQueue);
localQueue.clear();
// Check for auto-flush interval
const auto interval = autoFlushInterval.load();
if (interval > 0 && lastFlushTimer.elapsed_milliseconds() >= interval)
{
if (logFileStream.is_open())
{
logFileStream.flush();
}
lastFlushTimer.record();
}
}
}
void WriteEntries(const std::deque<std::string>& entries)
{
if (!logFileStream.is_open())
{
return;
}
for (const auto& entry : entries)
{
logFileStream.write(entry.c_str(), entry.length());
}
}
void FlushQueue()
{
std::unique_lock lock(queueMutex);
static thread_local std::deque<std::string> localQueue;
std::swap(localQueue, writeQueue);
lock.unlock();
WriteEntries(localQueue);
localQueue.clear();
if (logFileStream.is_open())
{
logFileStream.flush();
}
lastFlushTimer.record();
}
void Enqueue(const std::string& text)
{
{
std::scoped_lock lock(queueMutex);
writeQueue.push_back(text);
}
queueEntriesCondition.notify_one();
}
void FlushSync()
{
std::scoped_lock lock(flushMutex);
if (!initialized.load())
{
return;
}
// Signal and wait for queue to be processed
std::unique_lock queueLock(queueMutex);
if (!writeQueue.empty())
{
queueEntriesCondition.notify_one();
queueEmptyCondition.wait(queueLock, [this] { return writeQueue.empty(); });
}
queueLock.unlock();
// Force file flush
if (logFileStream.is_open())
{
logFileStream.flush();
}
}
void SetAutoFlushInterval(uint32_t ms)
{
autoFlushInterval = ms;
}
void UpdateLogFilePath(const std::string& newPath)
{
std::scoped_lock lock(flushMutex);
// Flush and close current file
FlushQueue();
if (logFileStream.is_open())
{
logFileStream.close();
}
// Open new file
currentLogFilePath = newPath;
logFileStream.open(newPath, std::ios::binary | std::ios::trunc);
}
};
static AsyncWriter asyncWriter;
struct InternalState
{
// These must have common lifetime and destruction order, so keep them together in a struct:
std::deque<LogEntry> entries;
std::mutex entriesLock;
InternalState() = default;
~InternalState()
{
asyncWriter.Stop();
}
InternalState(const InternalState&) = delete;
InternalState& operator=(const InternalState&) = delete;
InternalState(InternalState&&) = delete;
InternalState& operator=(InternalState&&) = delete;
std::string getText()
{
std::scoped_lock lck(entriesLock);
std::string retval;
_forEachLogEntry_unsafe([&](auto&& entry) {retval += entry.text;});
return retval;
}
void _forEachLogEntry_unsafe(const std::function<void(const LogEntry&)>& cb) const
{
for (auto& entry : entries)
{
cb(entry);
}
}
} internal_state;
void Flush()
{
asyncWriter.FlushSync();
}
void SetAutoFlushInterval(uint32_t milliseconds)
{
asyncWriter.SetAutoFlushInterval(milliseconds);
}
void Toggle()
{
enabled = !enabled;
was_ever_enabled = true;
}
void Scroll(float direction)
{
scroll += direction;
}
void Update(const wi::Canvas& canvas, float dt)
{
if (!locked)
{
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_HOME))
{
Toggle();
}
if (isActive())
{
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_UP))
{
historyPrev();
}
if (wi::input::Press(wi::input::KEYBOARD_BUTTON_DOWN))
{
historyNext();
}
if (wi::input::Down(wi::input::KEYBOARD_BUTTON_PAGEUP))
{
Scroll(1000.0f * dt);
}
if (wi::input::Down(wi::input::KEYBOARD_BUTTON_PAGEDOWN))
{
Scroll(-1000.0f * dt);
}
Scroll(wi::input::GetPointer().z * 20);
static bool created = false;
if (!created)
{
created = true;
inputField.Create("");
inputField.SetCancelInputEnabled(false);
inputField.OnInputAccepted([](const wi::gui::EventArgs& args) {
historyPos = 0;
post(args.sValue);
LogEntry entry;
entry.text = args.sValue;
entry.level = LogLevel::Default;
{
std::scoped_lock lock(historyLock);
history.push_back(entry);
if (history.size() > deleteFromLine)
{
history.pop_front();
}
}
if (!blockLuaExec)
{
wi::lua::RunText(args.sValue);
}
else
{
post("Lua execution is disabled", LogLevel::Error);
}
inputField.SetText("");
});
wi::Color theme_color_idle = wi::Color(30, 40, 60, 200);
wi::Color theme_color_focus = wi::Color(70, 150, 170, 220);
wi::Color theme_color_active = wi::Color::White();
wi::Color theme_color_deactivating = wi::Color::lerp(theme_color_focus, wi::Color::White(), 0.5f);
inputField.SetColor(theme_color_idle); // all states the same, it's gonna be always active anyway
inputField.font.params.color = wi::Color(160, 240, 250, 255);
inputField.font.params.shadowColor = wi::Color::Transparent();
toggleButton.Create("V");
toggleButton.OnClick([](const wi::gui::EventArgs& args) {
Toggle();
});
toggleButton.SetColor(theme_color_idle, wi::gui::IDLE);
toggleButton.SetColor(theme_color_focus, wi::gui::FOCUS);
toggleButton.SetColor(theme_color_active, wi::gui::ACTIVE);
toggleButton.SetColor(theme_color_deactivating, wi::gui::DEACTIVATING);
toggleButton.SetShadowRadius(5);
toggleButton.SetShadowColor(wi::Color(80, 140, 180, 100));
toggleButton.font.params.color = wi::Color(160, 240, 250, 255);
toggleButton.font.params.rotation = XM_PI;
toggleButton.font.params.size = 24;
toggleButton.font.params.scaling = 3;
toggleButton.font.params.shadowColor = wi::Color::Transparent();
for (int i = 0; i < arraysize(toggleButton.sprites); ++i)
{
toggleButton.sprites[i].params.enableCornerRounding();
toggleButton.sprites[i].params.corners_rounding[2].radius = 50;
}
}
if (inputField.GetState() != wi::gui::ACTIVE)
{
inputField.SetAsActive();
}
}
else
{
inputField.Deactivate();
}
}
if (enabled)
{
pos += speed * dt;
}
else
{
pos -= speed * dt;
}
pos = wi::math::Clamp(pos, -canvas.GetLogicalHeight(), 0);
inputField.SetSize(XMFLOAT2(canvas.GetLogicalWidth() - 40, 20));
inputField.SetPos(XMFLOAT2(20, canvas.GetLogicalHeight() - 40 + pos));
inputField.Update(canvas, dt);
toggleButton.SetSize(XMFLOAT2(100, 100));
toggleButton.SetPos(XMFLOAT2(canvas.GetLogicalWidth() - toggleButton.GetSize().x - 20, 20 + pos));
toggleButton.Update(canvas, dt);
}
void Draw(
const wi::Canvas& canvas,
CommandList cmd,
ColorSpace colorspace
)
{
if (!was_ever_enabled || pos <= -canvas.GetLogicalHeight())
{
return;
}
GraphicsDevice* device = GetDevice();
device->EventBegin("Backlog", cmd);
wi::image::Params fx = wi::image::Params((float)canvas.GetLogicalWidth(), (float)canvas.GetLogicalHeight());
fx.pos = XMFLOAT3(0, pos, 0);
fx.opacity = wi::math::Lerp(0.9f, 0, saturate(-pos / canvas.GetLogicalHeight()));
if (colorspace != ColorSpace::SRGB)
{
fx.enableLinearOutputMapping(9);
}
fx.color = backgroundColor;
wi::image::Draw(backgroundTex.IsValid() ? &backgroundTex : nullptr, fx, cmd);
wi::image::Params inputbg;
inputbg.color = wi::Color(80, 140, 180, 200);
inputbg.pos = inputField.translation;
inputbg.pos.x -= 8;
inputbg.pos.y -= 8;
inputbg.siz = inputField.GetSize();
inputbg.siz.x += 16;
inputbg.siz.y += 16;
inputbg.enableCornerRounding();
inputbg.corners_rounding[0].radius = 10;
inputbg.corners_rounding[1].radius = 10;
inputbg.corners_rounding[2].radius = 10;
inputbg.corners_rounding[3].radius = 10;
if (colorspace != ColorSpace::SRGB)
{
inputbg.enableLinearOutputMapping(9);
}
wi::image::Draw(nullptr, inputbg, cmd);
if (colorspace != ColorSpace::SRGB)
{
inputField.sprites[inputField.GetState()].params.enableLinearOutputMapping(9);
inputField.font.params.enableLinearOutputMapping(9);
toggleButton.sprites[inputField.GetState()].params.enableLinearOutputMapping(9);
toggleButton.font.params.enableLinearOutputMapping(9);
}
inputField.Render(canvas, cmd);
wi::graphics::Rect rect;
rect.left = 0;
rect.right = (int32_t)canvas.GetPhysicalWidth();
rect.top = 0;
rect.bottom = (int32_t)canvas.GetPhysicalHeight();
device->BindScissorRects(1, &rect, cmd);
toggleButton.Render(canvas, cmd);
rect.bottom = int32_t(canvas.LogicalToPhysical(inputField.GetPos().y - 15));
device->BindScissorRects(1, &rect, cmd);
DrawOutputText(canvas, cmd, colorspace);
rect.left = 0;
rect.right = std::numeric_limits<int>::max();
rect.top = 0;
rect.bottom = std::numeric_limits<int>::max();
device->BindScissorRects(1, &rect, cmd);
device->EventEnd(cmd);
}
void DrawOutputText(
const wi::Canvas& canvas,
CommandList cmd,
ColorSpace colorspace
)
{
wi::font::SetCanvas(canvas); // always set here as it can be called from outside...
wi::font::Params params = font_params;
params.cursor = {};
if (refitscroll)
{
const float textheight = wi::font::TextHeight(getText(), params);
const float limit = canvas.GetLogicalHeight() - 50;
if (scroll + textheight > limit)
{
scroll = limit - textheight;
}
refitscroll = false;
}
params.posX = 5;
params.posY = pos + scroll;
params.h_wrap = canvas.GetLogicalWidth() - params.posX;
if (colorspace != ColorSpace::SRGB)
{
params.enableLinearOutputMapping(9);
}
static std::deque<LogEntry> entriesCopy;
internal_state.entriesLock.lock();
// Force copy because drawing text while locking is not safe because an error inside might try to lock again!
entriesCopy = internal_state.entries;
internal_state.entriesLock.unlock();
for (auto& x : entriesCopy)
{
switch (x.level)
{
case LogLevel::Warning:
params.color = wi::Color::Warning();
break;
case LogLevel::Error:
params.color = wi::Color::Error();
break;
default:
params.color = font_params.color;
break;
}
params.cursor = wi::font::Draw(x.text, params, cmd);
}
unseen = LogLevel::None;
}
std::string getText()
{
return internal_state.getText();
}
// You generally don't want to use this. See notes in header
void _forEachLogEntry_unsafe(const std::function<void(const LogEntry&)>& cb)
{
internal_state._forEachLogEntry_unsafe(cb);
}
void clear()
{
std::scoped_lock lck(internal_state.entriesLock);
internal_state.entries.clear();
scroll = 0;
}
void post(const char* input, LogLevel level)
{
if (logLevel > level)
{
return;
}
// Initialize async writer on first log
if (!asyncWriter.initialized.load())
{
asyncWriter.Start(GetLogFile());
}
std::string str;
switch (level)
{
default:
case LogLevel::Default:
str = "";
break;
case LogLevel::Warning:
str = "[Warning] ";
break;
case LogLevel::Error:
str = "[Error] ";
break;
}
str += input;
str += '\n';
LogEntry entry;
entry.text = str;
entry.level = level;
// Add to in-memory display buffer
internal_state.entriesLock.lock();
internal_state.entries.push_back(entry);
if (internal_state.entries.size() > deleteFromLine)
{
internal_state.entries.pop_front();
}
internal_state.entriesLock.unlock();
// Enqueue for async file writing
asyncWriter.Enqueue(str);
refitscroll = true;
switch (level)
{
default:
case LogLevel::Default:
wi::helper::DebugOut(str, wi::helper::DebugLevel::Normal);
break;
case LogLevel::Warning:
wi::helper::DebugOut(str, wi::helper::DebugLevel::Warning);
break;
case LogLevel::Error:
wi::helper::DebugOut(str, wi::helper::DebugLevel::Error);
break;
}
unseen = std::max(unseen, level);
// Force an immediate flush on errors to prevent potential data loss
// in case the application is about to crash
if (level >= LogLevel::Error)
{
asyncWriter.FlushSync();
}
}
void post(const std::string& input, LogLevel level)
{
post(input.c_str(), level);
}
void historyPrev()
{
std::scoped_lock lock(historyLock);
if (!history.empty())
{
inputField.SetText(history[history.size() - 1 - historyPos].text);
inputField.SetAsActive();
if ((size_t)historyPos < history.size() - 1)
{
historyPos++;
}
}
}
void historyNext()
{
std::scoped_lock lock(historyLock);
if (!history.empty())
{
if (historyPos > 0)
{
historyPos--;
}
inputField.SetText(history[history.size() - 1 - historyPos].text);
inputField.SetAsActive();
}
}
void setBackground(Texture* texture)
{
backgroundTex = *texture;
}
void setBackgroundColor(wi::Color color)
{
backgroundColor = color;
}
void setFontSize(int value)
{
font_params.size = value;
}
void setFontRowspacing(float value)
{
font_params.spacingY = value;
}
void setFontColor(wi::Color color)
{
font_params.color = color;
}
bool isActive() { return enabled; }
void Lock()
{
locked = true;
enabled = false;
}
void Unlock()
{
locked = false;
}
void BlockLuaExecution()
{
blockLuaExec = true;
}
void UnblockLuaExecution()
{
blockLuaExec = false;
}
void SetLogLevel(LogLevel newLevel)
{
logLevel = newLevel;
}
LogLevel GetUnseenLogLevelMax()
{
return unseen;
}
void SetLogFile(const std::string& path)
{
logfile_path = path;
if (asyncWriter.initialized.load())
{
asyncWriter.UpdateLogFilePath(GetLogFile());
}
}
void GetLogFile(std::string& path)
{
if (!logfile_path.empty())
{
path = logfile_path;
}
}
std::string GetLogFile()
{
if (logfile_path.empty() || !wi::helper::DirectoryExists(wi::helper::GetDirectoryFromPath(logfile_path)))
{
return wi::helper::GetCurrentPath() + "/log.txt";
}
return logfile_path;
}
}