From a97387a50b2fc7fea275313b06b33affff12719d Mon Sep 17 00:00:00 2001 From: Maxwell Iverson <54956548+maxwelliverson@users.noreply.github.com> Date: Tue, 1 Oct 2024 18:40:56 -0700 Subject: [PATCH] feat: osltoy - Add command line and GUI ways to adjust include search paths (#1876) For Dev Days #1862 Addressing a osltoy feature request, I've added the ability to specify custom search paths. The changes consist of the following: - Added a command line option `-I ` to specify search paths (`OIIO::ArgParse` unfortunately can't mirror `OSLCompiler`'s `-Ipath` syntax) - OSLToyMainWindow now stores an array of `OSLCompiler` options alongside a flag indicating whether or not they should be regenerated before compilation. The only option currently able to be specified is `-Ipath`, but this should ease any future extension. - Added a GUI component consiting of a window with a mutable list of the current set of search paths. This list is initially populated with any paths passed via the command line, but any modifications to the list will be reflected when the shader is next recompiled. Modifying this list does not currently force recompilation. - Added an entry to the Tools menu to open the new GUI component. - Added a line of output to the error window indicating what options the compiler was invoked with. I've defaulted to having it print for every run, but this may want to only print on failure. ## Tests I did not add any automated tests for this feature, as is consistent with the rest of osltoy. I did, however, do some manual testing to make sure the functionality works as expected. Shaders that failed to `#include` some file instead succeeded when the parent directory of that file was added to the list of search paths via both the command line and the GUI component. Respectively, when said search path is then removed via the GUI component, any subsequent compilation attempts fail again. The GUI component otherwise behaves as expected. --------- Signed-off-by: Maxwell Iverson --- src/osltoy/osltoyapp.cpp | 312 +++++++++++++++++++++++++++++++++++++- src/osltoy/osltoyapp.h | 26 +++- src/osltoy/osltoymain.cpp | 7 + 3 files changed, 332 insertions(+), 13 deletions(-) diff --git a/src/osltoy/osltoyapp.cpp b/src/osltoy/osltoyapp.cpp index 3e6a59d08..c628d67fa 100644 --- a/src/osltoy/osltoyapp.cpp +++ b/src/osltoy/osltoyapp.cpp @@ -32,6 +32,7 @@ #include #include #include +#include // QT's extension foreach defines a foreach macro which interferes // with an OSL internal foreach method. So we will undefine it here @@ -351,6 +352,241 @@ class OSLToyRenderView final : public QLabel { #endif }; + + +class OSLToySearchPathLine final : public QLineEdit { + // Q_OBJECT +public: + explicit OSLToySearchPathLine(OSLToySearchPathEditor* editor, int index); + + bool previouslyHadContent() const { return m_previouslyHadContent; } + + void setPreviouslyHadContent(bool value) { m_previouslyHadContent = value; } + + int getIndex() const { return m_index; } + + QSize sizeHint() const override; + +private: + static QColor getColor(int index) + { + if (index % 2) + return QColor(0xFFE0F0FF); // light blue + else + return Qt::white; + } + + bool m_previouslyHadContent = false; + int m_index; + OSLToySearchPathEditor* m_editor = nullptr; +}; + + + +// More generically, this is a popup window with a list (that grows as needed) of editable text items. +class OSLToySearchPathEditor final : public QWidget { + using UpdatePathListAction + = std::function&)>; + +public: + OSLToySearchPathEditor(QWidget* parent, + UpdatePathListAction updatePathsAction) + : QWidget(parent, static_cast( + Qt::Tool | Qt::WindowStaysOnTopHint)) + , m_lines() + , m_updateAction(updatePathsAction) + { + window()->setWindowTitle(tr("#include Search Path List")); + + int thisWidth = parent->width(); // / 3; + int thisHeight = parent->height(); // / 4; + resize(thisWidth, thisHeight); + setFixedSize(size()); + + class MyScrollArea : public QScrollArea { + QWidget* m_parent; + + public: + explicit MyScrollArea(QWidget* parent) + : QScrollArea(parent), m_parent(parent) + { + } + + QSize sizeHint() const override { return m_parent->size(); } + }; + + class MyFrame : public QFrame { + QWidget* m_parent; + + public: + explicit MyFrame(QWidget* parent) : QFrame(parent), m_parent(parent) + { + } + + QSize sizeHint() const override { return m_parent->size(); } + }; + + auto scroll_area = new MyScrollArea(this); + scroll_area->setWidgetResizable(true); + auto frame = new MyFrame(scroll_area); + auto layout = new QVBoxLayout(); + layout->setSpacing(0); + frame->setLayout(layout); + frame->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + scroll_area->setWidget(frame); + scroll_area->show(); + m_layout = layout; + m_scrollArea = scroll_area; + } + + void set_path_list(const std::vector& paths) + { + while (!m_lines.empty()) + pop_line(); + m_maxIndexWithContent + = (int)paths.size() + - 1; // ok that this is -1 if the paths are empty + auto initialLineCount = required_lines(); + m_lines.reserve(initialLineCount); + while (m_lines.size() < initialLineCount) + push_line(); + for (size_t i = 0; i < paths.size(); ++i) + m_lines[i]->setText(QString::fromStdString(paths[i])); + update_path_list(); + } + + void observe_changed_text() + { + // Only listen to signals from OSLToySearchPathLine objects + if (auto changedLine = dynamic_cast(sender())) { + bool isNowEmpty = changedLine->text().isEmpty(); + if (changedLine->previouslyHadContent() && isNowEmpty) { + if (changedLine->getIndex() == m_maxIndexWithContent) { + // Find the next max index with content, or -1 if none. + do { + --m_maxIndexWithContent; + } while (m_maxIndexWithContent >= 0 + && !m_lines[m_maxIndexWithContent] + ->previouslyHadContent()); + + shrink_as_needed(); + } + } else if (!changedLine->previouslyHadContent() && !isNowEmpty) { + if (changedLine->getIndex() > m_maxIndexWithContent) { + m_maxIndexWithContent = changedLine->getIndex(); + grow_as_needed(); + } + } + + changedLine->setPreviouslyHadContent(!isNowEmpty); + } + } + +protected: + void closeEvent(QCloseEvent* ev) override + { + // On close, collate the list of search paths, and if there has been any change, update. + bool has_updated = false; + for (auto line : m_lines) { + if (line->isModified()) { + if (!has_updated) { + update_path_list(); + has_updated = true; + } + line->setModified(false); + } + } + + ev->accept(); + } + +private: + void push_line() + { + auto l = new OSLToySearchPathLine(this, (int)m_lines.size()); + m_layout->addWidget(l); + m_lines.push_back(l); + } + + void pop_line() + { + auto line = m_lines.back(); + m_lines.pop_back(); + m_layout->removeWidget(line); + } + + void update_path_list() + { + std::vector path_list; + for (auto line : m_lines) { + auto&& text = line->text(); + if (!text.isEmpty()) + path_list.push_back(text.toStdString()); + } + m_updateAction(path_list); + } + + size_t required_lines() const + { + return static_cast( + (std::max)(m_minLineCount, + m_maxIndexWithContent + m_guaranteedEmptyLineCount + 1)); + } + + void grow_as_needed() + { + auto newReqLines = required_lines(); + while (m_lines.size() < newReqLines) { + push_line(); + } + } + + void shrink_as_needed() + { + auto newReqLines = required_lines(); + while (m_lines.size() > newReqLines) { + pop_line(); + } + } + + int m_minLineCount = 12; + int m_guaranteedEmptyLineCount = 5; + int m_maxIndexWithContent = -1; + std::vector m_lines; + QLayout* m_layout = nullptr; + QScrollArea* m_scrollArea = nullptr; + UpdatePathListAction m_updateAction; +}; + + + +OSLToySearchPathLine::OSLToySearchPathLine(OSLToySearchPathEditor* editor, + int index) + : QLineEdit(), m_index(index), m_editor(editor) +{ + setFrame(true); + + auto p = this->palette(); + p.setColor(QPalette::Base, getColor(index)); + setPalette(p); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + QObject::connect(this, &OSLToySearchPathLine::editingFinished, editor, + &OSLToySearchPathEditor::observe_changed_text); + + // Maybe add a QCompleter that completes known paths + show(); +} + + + +QSize +OSLToySearchPathLine::sizeHint() const +{ + return QSize(m_editor->width() - 4, 10); +} + + + void #if OSL_QT_MAJOR < 6 Magnifier::enterEvent(QEvent* event) @@ -402,9 +638,6 @@ OSLToyMainWindow::OSLToyMainWindow(OSLToyRenderer* rend, int xr, int yr) setWindowTitle(tr("OSL Toy")); - // Set size of the window - // setFixedSize(100, 50); - createActions(); createMenus(); createStatusBar(); @@ -458,6 +691,11 @@ OSLToyMainWindow::OSLToyMainWindow(OSLToyRenderer* rend, int xr, int yr) &OSLToyMainWindow::restart_time); control_area_layout->addWidget(restartButton); + searchPathEditor + = new OSLToySearchPathEditor(this, [this](auto&& paths) mutable { + update_include_search_paths(paths); + }); + auto editorarea = new QWidget; QFontMetrics fontmetrics(CodeEditor::fixedFont()); #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) @@ -517,6 +755,9 @@ OSLToyMainWindow::createActions() &OSLToyMainWindow::recompile_shaders); add_action("Enter Full Screen", "", "", &OSLToyMainWindow::action_fullscreen); + add_action("search-path-popup", "Edit #include search paths...", + "Shift-Ctrl+P", + &OSLToyMainWindow::action_open_search_path_popup); } @@ -553,6 +794,7 @@ OSLToyMainWindow::createMenus() toolsMenu = new QMenu(tr("&Tools"), this); toolsMenu->addAction(actions["Recompile shaders"]); + toolsMenu->addAction(actions["search-path-popup"]); menuBar()->addMenu(toolsMenu); helpMenu = new QMenu(tr("&Help"), this); @@ -711,6 +953,23 @@ OSLToyMainWindow::open_file(const std::string& filename) } +void +OSLToyMainWindow::set_include_search_paths(const std::vector& paths) +{ + searchPathEditor->set_path_list(paths); +} + +void +OSLToyMainWindow::update_include_search_paths( + const std::vector& paths) +{ + m_include_search_paths = paths; + m_should_regenerate_compile_options = true; + + // Open question: Do we want to force a recompile whenever the list is updated? + // For now, I'm defaulting to no, but this is just a guess. +} + void OSLToyMainWindow::action_saveas() @@ -758,6 +1017,17 @@ OSLToyMainWindow::action_save() +void +OSLToyMainWindow::action_open_search_path_popup() +{ + auto centeredXPos = x() + (width() - searchPathEditor->width()) / 2; + auto centeredYPos = y() + (height() - searchPathEditor->height()) / 2; + searchPathEditor->move(centeredXPos, centeredYPos); + searchPathEditor->show(); +} + + + // Separate thread pool just for the async render kickoff triggers, but use // the default pool for the workers. static OIIO::thread_pool trigger_pool; @@ -836,6 +1106,24 @@ class MyOSLCErrorHandler final : public OIIO::ErrorHandler { }; +void +OSLToyMainWindow::regenerate_compile_options() +{ + // Right now, the only option we consider is include search path (-I) + + // Annoyingly, oslcomp only supports -I flags without any seperator between + // the -I and the path itself, but OIIO::ArgParse does not support parsing + // arguments in this manner. Oy vey. + + m_compile_options.clear(); + + for (auto&& path : m_include_search_paths) + m_compile_options.push_back(std::string("-I").append(path)); + + + m_should_regenerate_compile_options = false; +} + void OSLToyMainWindow::recompile_shaders() @@ -863,11 +1151,19 @@ OSLToyMainWindow::recompile_shaders() MyOSLCErrorHandler errhandler(this); OSLCompiler oslcomp(&errhandler); std::string osooutput; - std::vector options; - ok = oslcomp.compile_buffer(source, osooutput, options, "", - briefname); - set_error_message(tab, - OIIO::Strutil::join(errhandler.errors, "\n")); + + if (m_should_regenerate_compile_options) + regenerate_compile_options(); + + ok = oslcomp.compile_buffer(source, osooutput, m_compile_options, + "", briefname); + + auto error_message = OIIO::Strutil::fmt::format( + "{}\n\nCompiled {} with options: {}", + OIIO::Strutil::join(errhandler.errors, "\n"), briefname, + OIIO::Strutil::join(m_compile_options, " ")); + set_error_message(tab, error_message); + if (ok) { // std::cout << osooutput << "\n"; ok = shadingsys()->LoadMemoryCompiledShader(briefname, diff --git a/src/osltoy/osltoyapp.h b/src/osltoy/osltoyapp.h index 3d4055ee8..14a0d3f40 100644 --- a/src/osltoy/osltoyapp.h +++ b/src/osltoy/osltoyapp.h @@ -57,6 +57,7 @@ class ParamRec final : public OSLQuery::Parameter { }; class OSLToyRenderView; +class OSLToySearchPathEditor; class OSLToyMainWindow final : public QMainWindow { Q_OBJECT @@ -91,6 +92,9 @@ class OSLToyMainWindow final : public QMainWindow { bool open_file(const std::string& filename); + void set_include_search_paths(const std::vector& paths); + void update_include_search_paths(const std::vector& paths); + void rerender_needed() { m_rerender_needed = 1; } private slots: @@ -102,11 +106,12 @@ private slots: // Non-owning pointers to all the widgets we create. Qt is responsible // for deleting. QSplitter* centralSplitter; - OSLToyRenderView* renderView = nullptr; - QTabWidget* textTabs = nullptr; - QScrollArea* paramScroll = nullptr; - QWidget* paramWidget = nullptr; - QGridLayout* paramLayout = nullptr; + OSLToyRenderView* renderView = nullptr; + OSLToySearchPathEditor* searchPathEditor = nullptr; + QTabWidget* textTabs = nullptr; + QScrollArea* paramScroll = nullptr; + QWidget* paramWidget = nullptr; + QGridLayout* paramLayout = nullptr; QLabel* statusFPS; QMenu *fileMenu, *editMenu, *viewMenu, *toolsMenu, *helpMenu; QPushButton *recompileButton, *pauseButton, *restartButton; @@ -165,6 +170,8 @@ private slots: void action_fullscreen() {} void action_about() {} + void action_open_search_path_popup(); + void set_ui_to_paramval(ParamRec* param); void reset_param_to_default(ParamRec* param); void set_param_instance_value(ParamRec* param); @@ -183,6 +190,8 @@ private slots: void make_param_adjustment_row(ParamRec* param, QGridLayout* layout, int row); + void regenerate_compile_options(); + void rebuild_param_area(); void inventory_params(); OIIO::ImageBuf& framebuffer(); @@ -197,6 +206,13 @@ private slots: std::string m_groupname; bool m_shader_uses_time = false; + std::vector m_include_search_paths; + + + bool m_should_regenerate_compile_options = true; + std::vector m_compile_options; + + // Access control mutex for handing things off between the GUI thread // and the shading thread. OIIO::spin_mutex m_job_mutex; diff --git a/src/osltoy/osltoymain.cpp b/src/osltoy/osltoymain.cpp index 9b926e356..661d51171 100644 --- a/src/osltoy/osltoymain.cpp +++ b/src/osltoy/osltoymain.cpp @@ -39,6 +39,7 @@ static bool foreground_mode = true; static int threads = 0; static int xres = 512, yres = 512; static std::vector filenames; +static std::vector include_paths; static void @@ -58,6 +59,9 @@ getargs(int argc, char* argv[]) .help("Set thread count (0=cores)"); ap.arg("--res %d:XRES %d:YRES", &xres, &yres) .help("Set resolution"); + ap.arg("-I DIRPATH") + .action([&](cspan argv){ include_paths.emplace_back(argv[1]); }) + .help("Add DIRPATH to the list of header search paths."); // clang-format on if (ap.parse(argc, (const char**)argv) < 0) { std::cerr << ap.geterror() << std::endl; @@ -94,6 +98,9 @@ main(int argc, char* argv[]) QApplication app(argc, argv); OSLToyMainWindow mainwin(rend, xres, yres); mainwin.show(); + + mainwin.set_include_search_paths(include_paths); + for (auto&& filename : filenames) mainwin.open_file(filename);