From caa741837565e51d8bc6f00f9093f034fc821d1c Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 19 Jun 2023 15:38:16 -0400 Subject: [PATCH] async threading for map progress, etc. no cancelling yet --- common/log.cc | 45 ++++++++- include/common/log.hh | 13 +++ include/light/light.hh | 1 + light/bounce.cc | 24 +++-- light/light.cc | 4 +- light/surflight.cc | 24 ++++- lightpreview/mainwindow.cpp | 181 ++++++++++++++++++++++++++++++------ lightpreview/mainwindow.h | 47 +++++++++- 8 files changed, 290 insertions(+), 49 deletions(-) diff --git a/common/log.cc b/common/log.cc index d7cb3a26..e1dfa8c2 100644 --- a/common/log.cc +++ b/common/log.cc @@ -79,6 +79,12 @@ void close() } static std::mutex print_mutex; +static print_callback_t active_print_callback; + +void set_print_callback(print_callback_t cb) +{ + active_print_callback = cb; +} void print(flag logflag, const char *str) { @@ -86,6 +92,12 @@ void print(flag logflag, const char *str) return; } + if (active_print_callback) + { + active_print_callback(logflag, str); + return; + } + fmt::text_style style; if (enable_color_codes) { @@ -157,6 +169,13 @@ void assert_(bool success, const char *expr, const char *file, int line) } } +static percent_callback_t active_percent_callback; + +void set_percent_callback(percent_callback_t cb) +{ + active_percent_callback = cb; +} + void percent(uint64_t count, uint64_t max, bool displayElapsed) { bool expected = false; @@ -188,9 +207,17 @@ void percent(uint64_t count, uint64_t max, bool displayElapsed) is_timing = false; if (displayElapsed) { if (max == indeterminate) { - print(flag::PERCENT, "[done] time elapsed: {:.3}\n", elapsed); + if (active_percent_callback) { + active_percent_callback(std::nullopt, elapsed); + } else { + print(flag::PERCENT, "[done] time elapsed: {:.3}\n", elapsed); + } } else { - print(flag::PERCENT, "[100%] time elapsed: {:.3}\n", elapsed); + if (active_percent_callback) { + active_percent_callback(100u, elapsed); + } else { + print(flag::PERCENT, "[100%] time elapsed: {:.3}\n", elapsed); + } } } last_count = -1; @@ -198,7 +225,11 @@ void percent(uint64_t count, uint64_t max, bool displayElapsed) if (max != indeterminate) { uint32_t pct = static_cast((static_cast(count) / max) * 100); if (last_count != pct) { - print(flag::PERCENT, "[{:>3}%]\r", pct); + if (active_percent_callback) { + active_percent_callback(pct, std::nullopt); + } else { + print(flag::PERCENT, "[{:>3}%]\r", pct); + } last_count = pct; } } else { @@ -206,8 +237,12 @@ void percent(uint64_t count, uint64_t max, bool displayElapsed) if (t - last_indeterminate_time > std::chrono::milliseconds(100)) { constexpr const char *spinners[] = {". ", " . ", " . ", " ."}; - last_count = (last_count + 1) >= std::size(spinners) ? 0 : (last_count + 1); - print(flag::PERCENT, "[{}]\r", spinners[last_count]); + if (active_percent_callback) { + active_percent_callback(std::nullopt, std::nullopt); + } else { + last_count = (last_count + 1) >= std::size(spinners) ? 0 : (last_count + 1); + print(flag::PERCENT, "[{}]\r", spinners[last_count]); + } last_indeterminate_time = t; } } diff --git a/include/common/log.hh b/include/common/log.hh index 3ad8197c..e8f841ce 100644 --- a/include/common/log.hh +++ b/include/common/log.hh @@ -31,9 +31,12 @@ #include #include // for log10 #include // for std::runtime_error +#include // for std::function +#include // for std::optional #include #include #include +#include // forward declaration namespace settings @@ -89,6 +92,11 @@ inline void print(const char *formt, const Args &...args) print(flag::DEFAULT, fmt::format(fmt::runtime(formt), std::forward(args)...).c_str()); } +// set print callback +using print_callback_t = std::function; + +void set_print_callback(print_callback_t cb); + void header(const char *name); // TODO: C++20 source_location @@ -102,6 +110,11 @@ void header(const char *name); void assert_(bool success, const char *expr, const char *file, int line); +// set percent callback +using percent_callback_t = std::function percent, std::optional elapsed)>; + +void set_percent_callback(percent_callback_t cb); + // Display a percent timer. This also keeps track of how long the // current task is taking to execute. Note that only one of these // can be active at a time. Once `count` == `max`, the progress diff --git a/include/light/light.hh b/include/light/light.hh index c8e6ef1e..87f7af0f 100644 --- a/include/light/light.hh +++ b/include/light/light.hh @@ -253,6 +253,7 @@ enum class visapprox_t enum class emissivequality_t { LOW, + MEDIUM, HIGH }; diff --git a/light/bounce.cc b/light/bounce.cc index 651381c3..b80c6de6 100644 --- a/light/bounce.cc +++ b/light/bounce.cc @@ -222,23 +222,27 @@ static void MakeBounceLightsThread(const settings::worldspawn_keys &cfg, const m // Get face normal and midpoint... qvec3d facenormal = faceplane.normal; qvec3d facemidpoint = winding.center() + facenormal; // Lift 1 unit + + vector points; - if (light_options.emissivequality.value() == emissivequality_t::LOW) { - vector points{facemidpoint}; + if (light_options.emissivequality.value() == emissivequality_t::LOW || + light_options.emissivequality.value() == emissivequality_t::MEDIUM) { + points = {facemidpoint}; - for (auto &style : emitcolors) { - MakeBounceLight( - bsp, cfg, surf, style.second, style.first, points, winding, area, facenormal, facemidpoint); + if (light_options.emissivequality.value() == emissivequality_t::MEDIUM) { + + for (auto &pt : winding) { + points.push_back(pt + faceplane.normal); + } } } else { - vector points; winding.dice(cfg.bouncelightsubdivision.value(), [&points, &faceplane](winding_t &w) { points.push_back(w.center() + faceplane.normal); }); + } - for (auto &style : emitcolors) { - MakeBounceLight( - bsp, cfg, surf, style.second, style.first, points, winding, area, facenormal, facemidpoint); - } + for (auto &style : emitcolors) { + MakeBounceLight( + bsp, cfg, surf, style.second, style.first, points, winding, area, facenormal, facemidpoint); } } diff --git a/light/light.cc b/light/light.cc index 302f92d8..ba41d35f 100644 --- a/light/light.cc +++ b/light/light.cc @@ -300,8 +300,8 @@ light_settings::light_settings() this, "lightmap_scale", 0, &experimental_group, "force change lightmap scale; vanilla engines only allow 16"}, extra{ this, {"extra", "extra4"}, 1, &performance_group, "supersampling; 2x2 (extra) or 4x4 (extra4) respectively"}, - emissivequality{this, "emissivequality", emissivequality_t::LOW, { { "LOW", emissivequality_t::LOW }, { "HIGH", emissivequality_t::HIGH } }, &performance_group, - "low = one point in the center of the face, high = spread points out for antialiasing"}, + emissivequality{this, "emissivequality", emissivequality_t::LOW, { { "LOW", emissivequality_t::LOW }, { "MEDIUM", emissivequality_t::MEDIUM }, { "HIGH", emissivequality_t::HIGH } }, &performance_group, + "low = one point in the center of the face, med = center + all verts, high = spread points out for antialiasing"}, visapprox{this, "visapprox", visapprox_t::AUTO, {{"auto", visapprox_t::AUTO}, {"none", visapprox_t::NONE}, {"vis", visapprox_t::VIS}, {"rays", visapprox_t::RAYS}}, diff --git a/light/surflight.cc b/light/surflight.cc index a35de87f..7fffa954 100644 --- a/light/surflight.cc +++ b/light/surflight.cc @@ -123,10 +123,32 @@ static void MakeSurfaceLight(const mbsp_t *bsp, const settings::worldspawn_keys // Dice winding... l->points_before_culling = 0; - if (light_options.emissivequality.value() == emissivequality_t::LOW) { + if (light_options.emissivequality.value() == emissivequality_t::LOW || + light_options.emissivequality.value() == emissivequality_t::MEDIUM) { l->points = { l->pos }; l->points_before_culling++; total_surflight_points++; + + if (light_options.emissivequality.value() == emissivequality_t::MEDIUM) { + + for (auto &pt : winding) { + l->points_before_culling++; + auto point = pt + l->surfnormal; + auto diff = qv::normalize(l->pos - pt); + + point += diff; + + // optimization - cull surface lights in the void + // also try to move them if they're slightly inside a wall + auto [fixed_point, success] = FixLightOnFace(bsp, point, false, 0.5f); + if (!success) { + continue; + } + + l->points.push_back(fixed_point); + total_surflight_points++; + } + } } else { winding.dice(cfg.surflightsubdivision.value(), [&](winding_t &w) { ++l->points_before_culling; diff --git a/lightpreview/mainwindow.cpp b/lightpreview/mainwindow.cpp index 70f44390..0568b6cd 100644 --- a/lightpreview/mainwindow.cpp +++ b/lightpreview/mainwindow.cpp @@ -40,17 +40,21 @@ See file, 'COPYING', for details. #include #include #include +#include #include #include #include #include #include +#include +#include #include #include #include #include #include +#include #include "glview.h" @@ -95,6 +99,23 @@ static QStringList GetRecents() return recents; } +// ETLogWidget +ETLogWidget::ETLogWidget(QWidget *parent) : + QTabWidget(parent) +{ + for (size_t i = 0; i < std::size(logTabNames); i++) { + m_textEdits[i] = new QTextEdit(); + + auto *formLayout = new QFormLayout(); + auto *form = new QWidget(); + formLayout->addRow(m_textEdits[i]); + form->setLayout(formLayout); + setTabText(i, logTabNames[i]); + addTab(form, logTabNames[i]); + formLayout->setContentsMargins(0, 0, 0, 0); + } +} + // MainWindow MainWindow::MainWindow(QWidget *parent) @@ -223,16 +244,74 @@ void MainWindow::createPropertiesSidebar() m_fileReloadTimer->connect(m_fileReloadTimer.get(), &QTimer::timeout, this, &MainWindow::fileReloadTimerExpired); } +void MainWindow::logWidgetSetText(ETLogTab tab, const std::string &str) +{ + m_outputLogWidget->setTabText((int32_t) tab, str.c_str()); +} + +void MainWindow::lightpreview_percent_callback(std::optional percent, std::optional elapsed) +{ + int32_t tabIndex = (int32_t) m_activeLogTab; + + if (elapsed.has_value()) { + lightpreview_log_callback(logging::flag::PROGRESS, fmt::format("finished in: {:.3}\n", elapsed.value()).c_str()); + QMetaObject::invokeMethod(this, std::bind(&MainWindow::logWidgetSetText, this, m_activeLogTab, ETLogWidget::logTabNames[tabIndex])); + } else { + if (percent.has_value()) { + QMetaObject::invokeMethod(this, std::bind(&MainWindow::logWidgetSetText, this, m_activeLogTab, fmt::format("{} [{:>3}%]", ETLogWidget::logTabNames[tabIndex], percent.value()))); + } else { + QMetaObject::invokeMethod(this, std::bind(&MainWindow::logWidgetSetText, this, m_activeLogTab, fmt::format("{} (...)", ETLogWidget::logTabNames[tabIndex]))); + } + } +} + +void MainWindow::lightpreview_log_callback(logging::flag flags, const char *str) +{ + if (bitflags(flags) & logging::flag::PERCENT) + return; + + if (QApplication::instance()->thread() != QThread::currentThread()) + { + QMetaObject::invokeMethod(this, std::bind([this, flags](const std::string &s) -> void + { + lightpreview_log_callback(flags, s.c_str()); + }, std::string(str))); + return; + } + + auto *textEdit = m_outputLogWidget->textEdit(m_activeLogTab); + const bool atBottom = textEdit->verticalScrollBar()->value() == textEdit->verticalScrollBar()->maximum(); + QTextDocument* doc = textEdit->document(); + QTextCursor cursor(doc); + cursor.movePosition(QTextCursor::End); + cursor.beginEditBlock(); + cursor.insertBlock(); + cursor.insertHtml(QString::asprintf("%s\n", str)); + cursor.endEditBlock(); + + //scroll scrollarea to bottom if it was at bottom when we started + //(we don't want to force scrolling to bottom if user is looking at a + //higher position) + if (atBottom) { + QScrollBar* bar = textEdit->verticalScrollBar(); + bar->setValue(bar->maximum()); + } +} + void MainWindow::createOutputLog() { - QDockWidget *dock = new QDockWidget(tr("Output Log"), this); + QDockWidget *dock = new QDockWidget(tr("Tool Logs"), this); - m_outputTextEdit = new QTextEdit(); + m_outputLogWidget = new ETLogWidget(); // finish dock widget setup - dock->setWidget(m_outputTextEdit); + dock->setWidget(m_outputLogWidget); + addDockWidget(Qt::BottomDockWidgetArea, dock); viewMenu->addAction(dock->toggleViewAction()); + + logging::set_print_callback(std::bind(&MainWindow::lightpreview_log_callback, this, std::placeholders::_1, std::placeholders::_2)); + logging::set_percent_callback(std::bind(&MainWindow::lightpreview_percent_callback, this, std::placeholders::_1, std::placeholders::_2)); } void MainWindow::createStatusBar() @@ -386,9 +465,13 @@ std::filesystem::path MakeFSPath(const QString &string) return std::filesystem::path{string.toStdU16String()}; } -static bspdata_t QbspVisLight_Common(const std::filesystem::path &name, std::vector extra_qbsp_args, +bspdata_t MainWindow::QbspVisLight_Common(const std::filesystem::path &name, std::vector extra_qbsp_args, std::vector extra_vis_args, std::vector extra_light_args, bool run_vis) { + auto resetActiveTabText = [&]() { + QMetaObject::invokeMethod(this, std::bind(&MainWindow::logWidgetSetText, this, m_activeLogTab, ETLogWidget::logTabNames[(int32_t) m_activeLogTab])); + }; + auto bsp_path = name; bsp_path.replace_extension(".bsp"); @@ -401,12 +484,16 @@ static bspdata_t QbspVisLight_Common(const std::filesystem::path &name, std::vec args.push_back(name.string()); // run qbsp + m_activeLogTab = ETLogTab::TAB_BSP; InitQBSP(args); ProcessFile(); + resetActiveTabText(); + // run vis if (run_vis) { + m_activeLogTab = ETLogTab::TAB_VIS; std::vector vis_args{ "", // the exe path, which we're ignoring in this case }; @@ -417,8 +504,11 @@ static bspdata_t QbspVisLight_Common(const std::filesystem::path &name, std::vec vis_main(vis_args); } + resetActiveTabText(); + // run light { + m_activeLogTab = ETLogTab::TAB_LIGHT; std::vector light_args{ "", // the exe path, which we're ignoring in this case }; @@ -430,6 +520,10 @@ static bspdata_t QbspVisLight_Common(const std::filesystem::path &name, std::vec light_main(light_args); } + resetActiveTabText(); + + m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW; + // serialize obj { bspdata_t bspdata; @@ -515,30 +609,12 @@ private: GLView *glView; }; -void MainWindow::loadFileInternal(const QString &file, bool is_reload) +int MainWindow::compileMap(const QString &file, bool is_reload) { - qDebug() << "loadFileInternal " << file; - - // just in case - m_fileReloadTimer->stop(); - - // persist settings - QSettings s; - s.setValue("qbsp_options", qbsp_options->text()); - s.setValue("vis_enabled", vis_checkbox->isChecked()); - s.setValue("vis_options", vis_options->text()); - s.setValue("light_options", light_options->text()); - s.setValue("nearest", nearest->isChecked()); - - // update title bar - setWindowFilePath(file); - setWindowTitle(QFileInfo(file).fileName() + " - lightpreview"); - fs::path fs_path = MakeFSPath(file); m_bspdata = {}; - - settings::common_settings render_settings; + render_settings.reset(); try { if (fs_path.extension().compare(".bsp") == 0) { @@ -576,13 +652,22 @@ void MainWindow::loadFileInternal(const QString &file, bool is_reload) m_bspdata.loadversion->game->init_filesystem(file.toStdString(), settings); } } catch (const settings::parse_exception &p) { - m_outputTextEdit->append(QString::fromUtf8(p.what()) + QString::fromLatin1("\n")); - return; + auto *textEdit = m_outputLogWidget->textEdit(m_activeLogTab); + textEdit->append(QString::fromUtf8(p.what()) + QString::fromLatin1("\n")); + m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW; + return 1; } catch (const settings::quit_after_help_exception &p) { - m_outputTextEdit->append(QString::fromUtf8(p.what()) + QString::fromLatin1("\n")); - return; + auto *textEdit = m_outputLogWidget->textEdit(m_activeLogTab); + textEdit->append(QString::fromUtf8(p.what()) + QString::fromLatin1("\n")); + m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW; + return 1; } + return 0; +} + +void MainWindow::compileThreadExited() +{ const auto &bsp = std::get(m_bspdata.bsp); auto ents = EntData_Parse(bsp); @@ -590,9 +675,9 @@ void MainWindow::loadFileInternal(const QString &file, bool is_reload) // build lightmap atlas auto atlas = build_lightmap_atlas(bsp, m_bspdata.bspx.entries, false, bspx_decoupled_lm->isChecked()); - glView->renderBSP(file, bsp, m_bspdata.bspx.entries, ents, atlas, render_settings, bspx_normals->isChecked()); + glView->renderBSP(m_mapFile, bsp, m_bspdata.bspx.entries, ents, atlas, render_settings, bspx_normals->isChecked()); - if (!is_reload && !glView->getKeepOrigin()) { + if (!m_fileWasReload && !glView->getKeepOrigin()) { for (auto &ent : ents) { if (ent.get("classname") == "info_player_start") { qvec3d origin; @@ -624,6 +709,42 @@ void MainWindow::loadFileInternal(const QString &file, bool is_reload) auto *style = new QLightStyleSlider(style_entry.first, glView); lightstyles->addWidget(style); } + + delete m_compileThread; + m_compileThread = nullptr; +} + +void MainWindow::loadFileInternal(const QString &file, bool is_reload) +{ + // TODO + if (m_compileThread) + return; + + qDebug() << "loadFileInternal " << file; + + // just in case + m_fileReloadTimer->stop(); + m_fileWasReload = is_reload; + + // persist settings + QSettings s; + s.setValue("qbsp_options", qbsp_options->text()); + s.setValue("vis_enabled", vis_checkbox->isChecked()); + s.setValue("vis_options", vis_options->text()); + s.setValue("light_options", light_options->text()); + s.setValue("nearest", nearest->isChecked()); + + // update title bar + setWindowFilePath(file); + setWindowTitle(QFileInfo(file).fileName() + " - lightpreview"); + + for (auto &edit : m_outputLogWidget->textEdits()) { + edit->clear(); + } + + m_compileThread = QThread::create(std::bind(&MainWindow::compileMap, this, file, is_reload)); + connect(m_compileThread, &QThread::finished, this, &MainWindow::compileThreadExited); + m_compileThread->start(); } void MainWindow::displayCameraPositionInfo() diff --git a/lightpreview/mainwindow.h b/lightpreview/mainwindow.h index afd43d7d..b824b012 100644 --- a/lightpreview/mainwindow.h +++ b/lightpreview/mainwindow.h @@ -31,6 +31,40 @@ class QCheckBox; class QStringList; class QTextEdit; +enum class ETLogTab +{ + TAB_LIGHTPREVIEW, + TAB_BSP, + TAB_VIS, + TAB_LIGHT, + + TAB_TOTAL +}; + +class ETLogWidget : public QTabWidget +{ + Q_OBJECT + +public: + static constexpr const char *logTabNames[(size_t) ETLogTab::TAB_TOTAL] = { + "lightpreview", + "bsp", + "vis", + "light" + }; + + explicit ETLogWidget(QWidget *parent = nullptr); + ~ETLogWidget() { } + + QTextEdit *textEdit(ETLogTab i) { return m_textEdits[(size_t) i]; } + const QTextEdit *textEdit(ETLogTab i) const { return m_textEdits[(size_t) i]; } + + auto &textEdits() { return m_textEdits; } + +private: + std::array m_textEdits; +}; + class MainWindow : public QMainWindow { Q_OBJECT @@ -38,9 +72,13 @@ class MainWindow : public QMainWindow private: QFileSystemWatcher *m_watcher = nullptr; std::unique_ptr m_fileReloadTimer; + bool m_fileWasReload = false; QString m_mapFile; bspdata_t m_bspdata; + settings::common_settings render_settings; qint64 m_fileSize = -1; + ETLogTab m_activeLogTab = ETLogTab::TAB_LIGHTPREVIEW; + QThread *m_compileThread = nullptr; public: explicit MainWindow(QWidget *parent = nullptr); @@ -49,12 +87,19 @@ public: private: void createPropertiesSidebar(); void createOutputLog(); + void lightpreview_log_callback(logging::flag flags, const char *str); + void lightpreview_percent_callback(std::optional percent, std::optional elapsed); + void logWidgetSetText(ETLogTab tab, const std::string &str); void createStatusBar(); void updateRecentsSubmenu(const QStringList &recents); void setupMenu(); void fileOpen(); void takeScreenshot(); void fileReloadTimerExpired(); + int compileMap(const QString &file, bool is_reload); + void compileThreadExited(); + bspdata_t QbspVisLight_Common(const fs::path &name, std::vector extra_qbsp_args, + std::vector extra_vis_args, std::vector extra_light_args, bool run_vis); protected: void dragEnterEvent(QDragEnterEvent *event) override; @@ -83,5 +128,5 @@ private: QMenu *viewMenu = nullptr; QMenu *openRecentMenu = nullptr; - QTextEdit *m_outputTextEdit = nullptr; + ETLogWidget *m_outputLogWidget = nullptr; };