diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..ad2f5cfa --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(qsb:*)", + "Bash(ls:*)" + ] + } +} diff --git a/YACReader/YACReader.pro b/YACReader/YACReader.pro index 3eb06c23..3cc1aa76 100644 --- a/YACReader/YACReader.pro +++ b/YACReader/YACReader.pro @@ -27,10 +27,20 @@ INCLUDEPATH += ../common \ !CONFIG(no_opengl) { INCLUDEPATH += ../common/gl + greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + INCLUDEPATH += ../common/rhi + DEFINES += YACREADER_USE_RHI + } } win32 { - LIBS += -loleaut32 -lole32 -lshell32 -lopengl32 -luser32 + LIBS += -loleaut32 -lole32 -lshell32 -luser32 + # When using RHI (Qt 6.7+), don't link OpenGL directly - QRhiWidget handles graphics APIs + greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + message("RHI mode: not linking opengl32 (using QRhiWidget)") + } else { + LIBS += -lopengl32 + } msvc { QMAKE_CXXFLAGS_RELEASE += /MP /Ob2 /Oi /Ot /GT /GL @@ -51,6 +61,10 @@ QT += network widgets core multimedia svg greaterThan(QT_MAJOR_VERSION, 5): QT += openglwidgets core5compat +greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + QT += gui-private +} + #CONFIG += release CONFIG -= flat @@ -93,6 +107,10 @@ HEADERS += ../common/comic.h \ !CONFIG(no_opengl) { HEADERS += ../common/gl/yacreader_flow_gl.h \ goto_flow_gl.h + + greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + HEADERS += ../common/rhi/yacreader_flow_rhi.h + } } SOURCES += ../common/comic.cpp \ @@ -132,6 +150,11 @@ SOURCES += ../common/comic.cpp \ !CONFIG(no_opengl) { SOURCES += ../common/gl/yacreader_flow_gl.cpp \ goto_flow_gl.cpp + + greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + SOURCES += ../common/rhi/yacreader_flow_rhi.cpp + RESOURCES += ../common/rhi/shaders/shaders.qrc + } } include(../custom_widgets/custom_widgets_yacreader.pri) diff --git a/YACReader/goto_flow_gl.cpp b/YACReader/goto_flow_gl.cpp index 323e368d..27772e21 100644 --- a/YACReader/goto_flow_gl.cpp +++ b/YACReader/goto_flow_gl.cpp @@ -15,17 +15,17 @@ GoToFlowGL::GoToFlowGL(QWidget *parent, FlowType flowType) : GoToFlowWidget(parent) { Q_UNUSED(flowType) - flow = new YACReaderPageFlowGL(this); + flow = new YACReaderPageFlowImpl(this); flow->setShowMarks(false); imageSize = Configuration::getConfiguration().getGotoSlideSize(); flow->setSlideSize(imageSize); - connect(flow, &YACReaderFlowGL::centerIndexChanged, this, &GoToFlowWidget::setPageNumber); - connect(flow, &YACReaderFlowGL::selected, this, &GoToFlowGL::goToPage); + connect(flow, &YACReaderPageFlowImpl::centerIndexChanged, this, &GoToFlowWidget::setPageNumber); + connect(flow, &YACReaderPageFlowImpl::selected, this, &GoToFlowGL::goToPage); connect(toolBar, &GoToFlowToolBar::goToPage, this, &GoToFlowGL::goToPage); - connect(toolBar, &GoToFlowToolBar::setCenter, flow, &YACReaderFlowGL::setCenterIndex); + connect(toolBar, &GoToFlowToolBar::setCenter, flow, &YACReaderPageFlowImpl::setCenterIndex); mainLayout->addWidget(flow); toolBar->raise(); diff --git a/YACReader/goto_flow_gl.h b/YACReader/goto_flow_gl.h index 34ede703..d2278641 100644 --- a/YACReader/goto_flow_gl.h +++ b/YACReader/goto_flow_gl.h @@ -3,7 +3,15 @@ #include "yacreader_global.h" #include "goto_flow_widget.h" + +// Conditional include based on Qt version and RHI availability +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) && defined(YACREADER_USE_RHI) +#include "yacreader_flow_rhi.h" +using YACReaderPageFlowImpl = YACReaderPageFlow3D; +#else #include "yacreader_flow_gl.h" +using YACReaderPageFlowImpl = YACReaderPageFlowGL; +#endif class QLineEdit; class QIntValidator; @@ -28,7 +36,7 @@ public: void setFlowRightToLeft(bool b) override; private: - YACReaderPageFlowGL *flow; + YACReaderPageFlowImpl *flow; void keyPressEvent(QKeyEvent *event) override; void resizeEvent(QResizeEvent *event) override; // Comic * comic; diff --git a/YACReader/viewer.cpp b/YACReader/viewer.cpp index b8ac3ccd..c4e4bfb5 100644 --- a/YACReader/viewer.cpp +++ b/YACReader/viewer.cpp @@ -86,6 +86,13 @@ Viewer::Viewer(QWidget *parent) // CONFIG GOTO_FLOW-------------------------------------------------------- #ifndef NO_OPENGL +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) && defined(YACREADER_USE_RHI) + // When using RHI, don't check OpenGL - assume hardware acceleration is available + bool openGLAvailable = true; + + if (!settings->contains(USE_OPEN_GL)) + settings->setValue(USE_OPEN_GL, 2); +#else OpenGLChecker openGLChecker; bool openGLAvailable = openGLChecker.hasCompatibleOpenGLVersion(); @@ -93,6 +100,7 @@ Viewer::Viewer(QWidget *parent) settings->setValue(USE_OPEN_GL, 2); else if (!openGLAvailable) settings->setValue(USE_OPEN_GL, 0); +#endif if ((settings->value(USE_OPEN_GL).toBool() == true)) goToFlow = new GoToFlowGL(this, Configuration::getConfiguration().getFlowType()); diff --git a/YACReaderLibrary/YACReaderLibrary.pro b/YACReaderLibrary/YACReaderLibrary.pro index 4e552b40..79d1ed17 100644 --- a/YACReaderLibrary/YACReaderLibrary.pro +++ b/YACReaderLibrary/YACReaderLibrary.pro @@ -20,8 +20,18 @@ include (../dependencies/pdf_backend.pri) INCLUDEPATH += ../common/gl +greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + INCLUDEPATH += ../common/rhi + DEFINES += YACREADER_USE_RHI +} win32 { - LIBS += -loleaut32 -lole32 -lshell32 -lopengl32 -luser32 + LIBS += -loleaut32 -lole32 -lshell32 -luser32 + # When using RHI (Qt 6.7+), don't link OpenGL directly - QRhiWidget handles graphics APIs + message("RHI mode: not linking opengl32 (using QRhiWidget)") + greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + } else { + LIBS += -lopengl32 + } msvc { QMAKE_CXXFLAGS_RELEASE += /MP /Ob2 /Oi /Ot /GT /GL @@ -51,6 +61,10 @@ QT += sql network widgets svg quickcontrols2 greaterThan(QT_MAJOR_VERSION, 5): QT += openglwidgets core5compat +greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + QT += gui-private +} + # Input HEADERS += comic_flow.h \ ../common/concurrent_queue.h \ @@ -140,6 +154,10 @@ HEADERS += comic_flow.h \ !CONFIG(no_opengl) { HEADERS += ../common/gl/yacreader_flow_gl.h + + greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + HEADERS += ../common/rhi/yacreader_flow_rhi.h + } } SOURCES += comic_flow.cpp \ @@ -228,6 +246,11 @@ SOURCES += comic_flow.cpp \ !CONFIG(no_opengl) { SOURCES += ../common/gl/yacreader_flow_gl.cpp + + greaterThan(QT_MAJOR_VERSION, 5):greaterThan(QT_MINOR_VERSION, 6) { + SOURCES += ../common/rhi/yacreader_flow_rhi.cpp + RESOURCES += ../common/rhi/shaders/shaders.qrc + } } macx { diff --git a/YACReaderLibrary/comic_flow_widget.cpp b/YACReaderLibrary/comic_flow_widget.cpp index 95515d91..f0bc487c 100644 --- a/YACReaderLibrary/comic_flow_widget.cpp +++ b/YACReaderLibrary/comic_flow_widget.cpp @@ -158,10 +158,15 @@ void ComicFlowWidgetSW::resortCovers(QList newOrder) ComicFlowWidgetGL::ComicFlowWidgetGL(QWidget *parent) : ComicFlowWidget(parent) { - flow = new YACReaderComicFlowGL(parent); +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) && defined(YACREADER_USE_RHI) + qDebug() << "ComicFlowWidgetGL: Creating YACReaderComicFlow3D (RHI implementation)"; +#else + qDebug() << "ComicFlowWidgetGL: Creating YACReaderComicFlowGL (OpenGL implementation)"; +#endif + flow = new YACReaderComicFlowImpl(this); - connect(flow, &YACReaderFlowGL::centerIndexChanged, this, &ComicFlowWidget::centerIndexChanged); - connect(flow, &YACReaderFlowGL::selected, this, &ComicFlowWidget::selected); + connect(flow, &YACReaderComicFlowImpl::centerIndexChanged, this, &ComicFlowWidget::centerIndexChanged); + connect(flow, &YACReaderComicFlowImpl::selected, this, &ComicFlowWidget::selected); auto l = new QVBoxLayout; l->addWidget(flow); diff --git a/YACReaderLibrary/comic_flow_widget.h b/YACReaderLibrary/comic_flow_widget.h index ace36488..98158592 100644 --- a/YACReaderLibrary/comic_flow_widget.h +++ b/YACReaderLibrary/comic_flow_widget.h @@ -6,7 +6,14 @@ #include "pictureflow.h" #include "comic_flow.h" #ifndef NO_OPENGL +// Conditional include based on Qt version and RHI availability +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) && defined(YACREADER_USE_RHI) +#include "yacreader_flow_rhi.h" +using YACReaderComicFlowImpl = YACReaderComicFlow3D; +#else #include "yacreader_flow_gl.h" +using YACReaderComicFlowImpl = YACReaderComicFlowGL; +#endif #endif class ComicFlowWidget : public QWidget { @@ -83,7 +90,7 @@ class ComicFlowWidgetGL : public ComicFlowWidget { Q_OBJECT private: - YACReaderComicFlowGL *flow; + YACReaderComicFlowImpl *flow; public: ComicFlowWidgetGL(QWidget *parent = nullptr); diff --git a/YACReaderLibrary/comics_view.cpp b/YACReaderLibrary/comics_view.cpp index b2cca088..88ac2e6e 100644 --- a/YACReaderLibrary/comics_view.cpp +++ b/YACReaderLibrary/comics_view.cpp @@ -17,10 +17,10 @@ ComicsView::ComicsView(QWidget *parent) view = new QQuickWidget(); - // QQuickWidget requires rendering into OpenGL framebuffer objects -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - view->quickWindow()->setGraphicsApi(QSGRendererInterface::OpenGL); -#endif + // In Qt 6, QQuickWidget supports Qt RHI and can use any graphics backend + // (Vulkan, Metal, Direct3D, OpenGL, or software rendering). + // The backend can be configured via QQuickWindow::setGraphicsApi() or QSG_RHI_BACKEND env var. + // Note: All widgets in the same top-level window must use the same graphics API. view->setResizeMode(QQuickWidget::SizeRootObjectToView); connect( diff --git a/YACReaderLibrary/folder_content_view.cpp b/YACReaderLibrary/folder_content_view.cpp index 679e8f2d..8d31844e 100644 --- a/YACReaderLibrary/folder_content_view.cpp +++ b/YACReaderLibrary/folder_content_view.cpp @@ -26,10 +26,10 @@ FolderContentView::FolderContentView(QAction *toogleRecentVisibilityAction, QWid view = new QQuickWidget(); - // QQuickWidget requires rendering into OpenGL framebuffer objects -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - view->quickWindow()->setGraphicsApi(QSGRendererInterface::OpenGL); -#endif + // In Qt 6, QQuickWidget supports Qt RHI and can use any graphics backend + // (Vulkan, Metal, Direct3D, OpenGL, or software rendering). + // The backend can be configured via QQuickWindow::setGraphicsApi() or QSG_RHI_BACKEND env var. + // Note: All widgets in the same top-level window must use the same graphics API. view->setResizeMode(QQuickWidget::SizeRootObjectToView); connect( diff --git a/YACReaderLibrary/library_window.cpp b/YACReaderLibrary/library_window.cpp index 78ac4f3a..158a2010 100644 --- a/YACReaderLibrary/library_window.cpp +++ b/YACReaderLibrary/library_window.cpp @@ -204,6 +204,12 @@ void LibraryWindow::setupOpenglSetting() // FLOW----------------------------------------------------------------------- //--------------------------------------------------------------------------- +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) && defined(YACREADER_USE_RHI) + // When using RHI, assume hardware acceleration is available + bool openGLAvailable = true; + if (!settings->contains(USE_OPEN_GL)) + settings->setValue(USE_OPEN_GL, 2); +#else OpenGLChecker openGLChecker; bool openGLAvailable = openGLChecker.hasCompatibleOpenGLVersion(); @@ -212,6 +218,7 @@ void LibraryWindow::setupOpenglSetting() else if (!openGLAvailable) settings->setValue(USE_OPEN_GL, 0); #endif +#endif } void LibraryWindow::setupUI() diff --git a/YACReaderLibrary/main.cpp b/YACReaderLibrary/main.cpp index 5e703c2d..e60e9ae6 100644 --- a/YACReaderLibrary/main.cpp +++ b/YACReaderLibrary/main.cpp @@ -83,8 +83,12 @@ void logSystemAndConfig() else QLOG_INFO() << "OpenGL : disabled"; +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) && defined(YACREADER_USE_RHI) + QLOG_INFO() << "Using RHI (Qt Rendering Hardware Interface) - graphics backend will be auto-selected"; +#else OpenGLChecker checker; QLOG_INFO() << "OpenGL version : " << checker.textVersionDescription(); +#endif auto libraries = DBHelper::getLibraries().getLibraries(); QLOG_INFO() << "Libraries: "; @@ -200,6 +204,8 @@ int main(int argc, char **argv) #endif parser.process(app); +// When using RHI (Qt 6.7+), don't allow OpenGL attribute overrides +#if !defined(YACREADER_USE_RHI) || QT_VERSION < QT_VERSION_CHECK(6, 7, 0) #ifdef Q_OS_WIN if (parser.isSet("opengl")) { QTextStream qout(stdout); @@ -216,6 +222,7 @@ int main(int argc, char **argv) parser.showHelp(); } } +#endif #endif if (parser.isSet("loglevel")) { diff --git a/common/opengl_checker.cpp b/common/opengl_checker.cpp index 2a73af96..a6d1840f 100644 --- a/common/opengl_checker.cpp +++ b/common/opengl_checker.cpp @@ -1,7 +1,5 @@ #include "opengl_checker.h" -#include "QsLog.h" - OpenGLChecker::OpenGLChecker() : compatibleOpenGLVersion(true) { diff --git a/common/rhi/README.md b/common/rhi/README.md new file mode 100644 index 00000000..fd124797 --- /dev/null +++ b/common/rhi/README.md @@ -0,0 +1,287 @@ +# YACReader Flow RHI Implementation + +This directory contains the QRhiWidget-based implementation of the YACReader 3D cover flow, providing cross-platform 3D rendering support for Qt 6.7+. + +## Overview + +The RHI (Rendering Hardware Interface) implementation is a modern replacement for the OpenGL-based flow (`yacreader_flow_gl`) that: + +- ✅ Supports **multiple graphics APIs**: Vulkan, Metal, Direct3D 11/12, OpenGL +- ✅ Provides **native performance** on modern platforms (Metal on macOS, D3D on Windows) +- ✅ Maintains **100% API compatibility** with the OpenGL version +- ✅ Works seamlessly with **Qt 6.7+** while Qt5 continues using OpenGL +- ✅ Enables **future-proof** rendering infrastructure + +## Architecture + +### Class Hierarchy + +``` +QRhiWidget (Qt base class) + └── YACReaderFlow3D (Base implementation) + ├── YACReaderComicFlow3D (File path-based loading for library) + └── YACReaderPageFlow3D (Byte array-based loading for viewer) +``` + +### Files + +- **yacreader_flow_rhi.h** - Header with class definitions +- **yacreader_flow_rhi.cpp** - Implementation +- **shaders/** - GLSL 450 shaders and compiled .qsb files +- **README.md** - This file + +## Key Features + +### Graphics Pipeline + +The implementation uses: +- **Instanced rendering** for efficient batch drawing +- **Dual-pass rendering** (reflections + covers) +- **MSAA** (4x by default) for anti-aliasing +- **Depth testing** and back-face culling +- **Alpha blending** for transparency + +### Resource Management + +QRhi resources managed: +- `QRhiBuffer` for vertices, instance data, and uniforms +- `QRhiTexture` for cover images, marks, and default texture +- `QRhiSampler` for texture filtering +- `QRhiGraphicsPipeline` for render state +- `QRhiShaderResourceBindings` for uniform/texture bindings + +### Shader System + +Shaders are written in **GLSL 4.50** and compiled to `.qsb` format supporting: +- OpenGL ES 2.0, 3.0 +- OpenGL 2.1, 3.0+ +- HLSL (Direct3D 11/12) +- Metal Shading Language (macOS/iOS) + +## Integration + +### Qt5 vs Qt6 Selection + +The build system automatically selects the appropriate implementation: + +**Qt 5.x**: Uses `YACReaderFlowGL` (OpenGL-based) +**Qt 6.0-6.6**: Uses `YACReaderFlowGL` (OpenGL-based) +**Qt 6.7+**: Uses `YACReaderFlow3D` (RHI-based) if `YACREADER_USE_RHI` is defined + +### Type Aliases + +Applications use type aliases for seamless switching: + +```cpp +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) && defined(YACREADER_USE_RHI) +using YACReaderPageFlowImpl = YACReaderPageFlow3D; +using YACReaderComicFlowImpl = YACReaderComicFlow3D; +#else +using YACReaderPageFlowImpl = YACReaderPageFlowGL; +using YACReaderComicFlowImpl = YACReaderComicFlowGL; +#endif +``` + +### Example Usage (YACReader Viewer) + +```cpp +// goto_flow_gl.cpp +flow = new YACReaderPageFlowImpl(this); +flow->setShowMarks(false); +flow->populate(numPages); +connect(flow, &YACReaderPageFlowImpl::selected, this, &GoToFlowGL::goToPage); +``` + +### Example Usage (YACReaderLibrary) + +```cpp +// comic_flow_widget.cpp +flow = new YACReaderComicFlowImpl(parent); +flow->setImagePaths(pathsList); +connect(flow, &YACReaderComicFlowImpl::centerIndexChanged, + this, &ComicFlowWidget::centerIndexChanged); +``` + +## API Compatibility + +All public methods from `YACReaderFlowGL` are preserved: + +### Navigation +- `showPrevious()`, `showNext()` +- `setCurrentIndex(int)`, `setCenterIndex(unsigned int)` +- `showSlide(int)`, `centerIndex()` + +### Configuration +- `setPreset(const Preset &)` +- `setPerformance(Performance)` +- `setFlowRightToLeft(bool)` +- `setZoom(int)`, `setRotation(int)` +- `setCF_RX/RY/RZ(int)`, `setCF_Y/Z(int)` +- `setX_Distance(int)`, `setCenter_Distance(int)`, etc. + +### Appearance +- `setBackgroundColor(const QColor &)` +- `setTextColor(const QColor &)` +- `setShadingColor(const QColor &)` +- `setShowMarks(bool)`, `setMarks(QVector<...>)` + +### Content Management +- `populate(int)`, `clear()`, `reset()`, `reload()` +- `insert()`, `remove(int)`, `add(int)`, `replace()` +- Subclass-specific: `setImagePaths()`, `resortCovers()`, etc. + +## Building + +### Prerequisites + +1. **Qt 6.7 or later** +2. **qsb tool** (Qt Shader Baker) in PATH +3. **C++17 compiler** + +### Compile Shaders + +Before building YACReader, compile the shaders: + +```bash +cd common/rhi/shaders +# Windows +compile_shaders.bat + +# Unix/macOS +chmod +x compile_shaders.sh +./compile_shaders.sh +``` + +This generates `flow.vert.qsb` and `flow.frag.qsb` which are embedded via `shaders.qrc`. + +### Build YACReader + +The `.pro` files automatically include RHI sources for Qt 6.7+: + +```bash +qmake YACReader.pro +make +``` + +For Qt 5 builds, the OpenGL version is used automatically. + +## Graphics API Selection + +QRhiWidget auto-selects the best API per platform: + +- **macOS/iOS**: Metal (native) +- **Windows**: Direct3D 11 (default) or Direct3D 12 +- **Linux**: Vulkan or OpenGL +- **Android**: OpenGL ES 3.0 or Vulkan + +You can force a specific API via environment variables (for testing): + +```bash +# Force OpenGL +export QSG_RHI_BACKEND=opengl + +# Force Vulkan +export QSG_RHI_BACKEND=vulkan + +# Force Direct3D 11 (Windows) +set QSG_RHI_BACKEND=d3d11 +``` + +## Performance + +Performance tiers match the OpenGL version: + +- **Low**: 8 covers loaded, 128px textures (page flow) / 200px (comic flow) +- **Medium**: 10 covers, 196px / 256px textures +- **High**: 12 covers, 256px / 320px textures +- **Ultra High**: 14-16 covers, full resolution textures + +Texture loading happens asynchronously via worker threads (`ImageLoader3D`, `ImageLoaderByteArray3D`). + +## Debugging + +### Enable Validation Layers + +For debugging graphics issues, enable validation: + +```cpp +flow->setDebugLayerEnabled(true); // Call before widget is shown +``` + +This activates: +- **Vulkan**: VK_LAYER_KHRONOS_validation +- **Direct3D**: D3D11 debug layer +- **Metal**: Metal API validation + +### Check Active Graphics API + +```cpp +QRhi *rhi = flow->rhi(); +qDebug() << "Backend:" << rhi->backend(); +qDebug() << "Driver:" << rhi->driverInfo(); +``` + +### Common Issues + +**Problem**: Shaders fail to load +**Solution**: Ensure `.qsb` files are compiled and included in resources + +**Problem**: Black screen on Qt 6.7 +**Solution**: Check if `YACREADER_USE_RHI` is defined in build + +**Problem**: Crashes on resize/reparent +**Solution**: `releaseResources()` properly cleans up all QRhi objects + +## Migration from OpenGL + +The RHI version is a **drop-in replacement** requiring no application code changes beyond the build system. + +### What's Different (Internal) + +| OpenGL API | QRhi Equivalent | +|------------|-----------------| +| `glDrawArraysInstanced()` | `cb->drawIndexed(instanceCount)` | +| `glUniform*()` | Update `QRhiBuffer` with uniform data | +| `glBindTexture()` | `QRhiShaderResourceBindings` | +| `glBlendFunc()` | `QRhiGraphicsPipeline::setTargetBlends()` | +| `glEnable(GL_DEPTH_TEST)` | `pipeline->setDepthTest(true)` | + +### What's the Same + +- All public API methods and signatures +- Animation system and timing +- Preset configurations +- Event handling (mouse, keyboard, wheel) +- Worker thread texture loading +- Performance tiers + +## Known Limitations + +1. **Qt Version**: Requires Qt 6.7+ (released April 2024) +2. **QRhi Stability**: QRhi APIs may change in minor Qt releases +3. **Mixed Renderers**: Only one graphics API per window +4. **Shader Compilation**: Must recompile shaders when modifying source + +## Future Improvements + +Potential enhancements: +- [ ] GPU-side frustum culling +- [ ] Compute shader for texture generation +- [ ] HDR/wide color gamut support +- [ ] Ray-traced reflections (via RHI compute) +- [ ] Dynamic LOD based on distance + +## References + +- [QRhiWidget Class Documentation](https://doc.qt.io/qt-6/qrhiwidget.html) +- [QRhi Overview](https://doc.qt.io/qt-6/qrhi.html) +- [Qt RHI Examples](https://doc.qt.io/qt-6/qtwidgets-rhi-cuberhiwidget-example.html) +- [Qt Shader Tools (qsb)](https://doc.qt.io/qt-6/qtshadertools-index.html) + +## License + +Same as YACReader project license. + +## Author + +Generated as part of the YACReader OpenGL modernization initiative. diff --git a/common/rhi/shaders/README.md b/common/rhi/shaders/README.md new file mode 100644 index 00000000..7b882820 --- /dev/null +++ b/common/rhi/shaders/README.md @@ -0,0 +1,49 @@ +# YACReader Flow RHI Shaders + +This directory contains the GLSL 4.50 shaders for the QRhiWidget-based flow implementation. + +## Files + +- `flow.vert` - Vertex shader (GLSL 450) +- `flow.frag` - Fragment shader (GLSL 450) +- `flow.vert.qsb` - Compiled vertex shader (multi-platform) +- `flow.frag.qsb` - Compiled fragment shader (multi-platform) +- `compile_shaders.bat` - Windows compilation script +- `compile_shaders.sh` - Unix/macOS compilation script +- `shaders.qrc` - Qt resource file + +## Compiling Shaders + +The shaders must be compiled to `.qsb` format using Qt's `qsb` tool before building YACReader. + +### Prerequisites + +Ensure `qsb` is in your PATH. It's typically located in: +- Windows: `C:\Qt\6.x.x\msvc2019_64\bin\qsb.exe` +- macOS: `/opt/Qt/6.x.x/macos/bin/qsb` +- Linux: `/opt/Qt/6.x.x/gcc_64/bin/qsb` + +### Compilation + +**Windows:** +```cmd +cd common/rhi/shaders +compile_shaders.bat +``` + +**Unix/macOS:** +```bash +cd common/rhi/shaders +chmod +x compile_shaders.sh +./compile_shaders.sh +``` + +The compiled `.qsb` files contain shader variants for: +- OpenGL ES 2.0, 3.0 +- OpenGL 2.1, 3.0+ +- HLSL (Direct3D 11/12) +- Metal Shading Language (macOS/iOS) + +## Note + +The `.qsb` files are included in the repository for convenience. Recompile only if you modify the shader source. diff --git a/common/rhi/shaders/compile_shaders.bat b/common/rhi/shaders/compile_shaders.bat new file mode 100644 index 00000000..56ce7381 --- /dev/null +++ b/common/rhi/shaders/compile_shaders.bat @@ -0,0 +1,19 @@ +@echo off +REM Compile shaders to .qsb format for Qt RHI +REM Requires qsb tool from Qt installation + +echo Compiling flow vertex shader... +qsb --glsl "100 es,120,150" --hlsl 50 --msl 12 -o flow.vert.qsb flow.vert +if %ERRORLEVEL% NEQ 0 ( + echo Error compiling vertex shader + exit /b 1 +) + +echo Compiling flow fragment shader... +qsb --glsl "100 es,120,150" --hlsl 50 --msl 12 -o flow.frag.qsb flow.frag +if %ERRORLEVEL% NEQ 0 ( + echo Error compiling fragment shader + exit /b 1 +) + +echo Shader compilation complete! diff --git a/common/rhi/shaders/compile_shaders.sh b/common/rhi/shaders/compile_shaders.sh new file mode 100644 index 00000000..f084121f --- /dev/null +++ b/common/rhi/shaders/compile_shaders.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Compile shaders to .qsb format for Qt RHI +# Requires qsb tool from Qt installation + +echo "Compiling flow vertex shader..." +qsb --glsl "100 es,120,150" --hlsl 50 --msl 12 -o flow.vert.qsb flow.vert +if [ $? -ne 0 ]; then + echo "Error compiling vertex shader" + exit 1 +fi + +echo "Compiling flow fragment shader..." +qsb --glsl "100 es,120,150" --hlsl 50 --msl 12 -o flow.frag.qsb flow.frag +if [ $? -ne 0 ]; then + echo "Error compiling fragment shader" + exit 1 +fi + +echo "Shader compilation complete!" diff --git a/common/rhi/shaders/flow.frag b/common/rhi/shaders/flow.frag new file mode 100644 index 00000000..01b474d9 --- /dev/null +++ b/common/rhi/shaders/flow.frag @@ -0,0 +1,44 @@ +#version 450 + +// Inputs from vertex shader +layout(location = 0) in vec2 vTexCoord; +layout(location = 1) in vec4 vColor; +layout(location = 2) in float vIsReflection; + +// Output +layout(location = 0) out vec4 fragColor; + +// Uniform buffer +layout(std140, binding = 0) uniform UniformBuffer +{ + mat4 viewProjectionMatrix; + vec3 backgroundColor; + float _pad0; + vec3 shadingColor; + float _pad1; + float reflectionUp; + float reflectionDown; + int isReflection; + float _pad2; +}; + +// Texture and sampler +layout(binding = 1) uniform sampler2D coverTexture; + +void main() +{ + vec4 texColor = texture(coverTexture, vTexCoord); + + // Apply shading: multiply texture by vColor.r to darken + float shadingAmount = vColor.r; + + // For reflections, apply gradient fade (darker at bottom, fading to black) + if (vIsReflection > 0.5) { + // vTexCoord.y goes from 1 (top of reflection, near cover) to 0 (bottom, far from cover) + // We want it brightest near the cover and fading away + float gradientFade = mix(0.0, 0.33, vTexCoord.y); + shadingAmount *= gradientFade; + } + + fragColor = vec4(texColor.rgb * shadingAmount, texColor.a); +} diff --git a/common/rhi/shaders/flow.frag.qsb b/common/rhi/shaders/flow.frag.qsb new file mode 100644 index 00000000..f50613bb Binary files /dev/null and b/common/rhi/shaders/flow.frag.qsb differ diff --git a/common/rhi/shaders/flow.vert b/common/rhi/shaders/flow.vert new file mode 100644 index 00000000..1584e783 --- /dev/null +++ b/common/rhi/shaders/flow.vert @@ -0,0 +1,59 @@ +#version 450 + +// Per-vertex attributes +layout(location = 0) in vec3 position; +layout(location = 1) in vec2 texCoord; + +// Per-instance attributes (mat4 split into 4 vec4s for better D3D11 compatibility) +layout(location = 2) in vec4 instanceModel_row0; +layout(location = 3) in vec4 instanceModel_row1; +layout(location = 4) in vec4 instanceModel_row2; +layout(location = 5) in vec4 instanceModel_row3; +layout(location = 6) in vec4 instanceShading1; +layout(location = 7) in float instanceOpacity; +layout(location = 8) in float instanceFlip; + +// Outputs to fragment shader +layout(location = 0) out vec2 vTexCoord; +layout(location = 1) out vec4 vColor; +layout(location = 2) out float vIsReflection; + +// Uniform buffer +layout(std140, binding = 0) uniform UniformBuffer +{ + mat4 viewProjectionMatrix; + vec3 backgroundColor; + float _pad0; + vec3 shadingColor; + float _pad1; + float reflectionUp; + float reflectionDown; + int isReflection; + float _pad2; +}; + +void main() +{ + // Reconstruct instance model matrix from 4 vec4 rows + mat4 instanceModel = mat4(instanceModel_row0, instanceModel_row1, instanceModel_row2, instanceModel_row3); + + gl_Position = viewProjectionMatrix * instanceModel * vec4(position, 1.0); + vTexCoord = texCoord; + + // Flip texture vertically per-instance when requested (reflection) + if (instanceFlip != 0.0) { + vTexCoord.y = 1.0 - texCoord.y; + } + + float leftUpShading = instanceShading1.x; + float leftDownShading = instanceShading1.y; + float rightUpShading = instanceShading1.z; + float rightDownShading = instanceShading1.w; + + float leftShading = mix(leftDownShading, leftUpShading, (position.y + 0.5)); + float rightShading = mix(rightDownShading, rightUpShading, (position.y + 0.5)); + float shading = mix(leftShading, rightShading, (position.x + 0.5)); + + vColor = vec4(shading * instanceOpacity); + vIsReflection = instanceFlip; +} diff --git a/common/rhi/shaders/flow.vert.qsb b/common/rhi/shaders/flow.vert.qsb new file mode 100644 index 00000000..11aa806e Binary files /dev/null and b/common/rhi/shaders/flow.vert.qsb differ diff --git a/common/rhi/shaders/shaders.qrc b/common/rhi/shaders/shaders.qrc new file mode 100644 index 00000000..37a0d50b --- /dev/null +++ b/common/rhi/shaders/shaders.qrc @@ -0,0 +1,6 @@ + + + flow.vert.qsb + flow.frag.qsb + + diff --git a/common/rhi/yacreader_flow_rhi.cpp b/common/rhi/yacreader_flow_rhi.cpp new file mode 100644 index 00000000..dc3f7c49 --- /dev/null +++ b/common/rhi/yacreader_flow_rhi.cpp @@ -0,0 +1,1744 @@ +#include "yacreader_flow_rhi.h" +#include +#include + +// Structure for per-instance data +struct InstanceData { + QMatrix4x4 modelMatrix; + float leftUpShading; + float leftDownShading; + float rightUpShading; + float rightDownShading; + float opacity; + float padding[3]; // Align to 16 bytes +}; + +/*** Preset Configurations ***/ +// Note: The preset configurations are already defined in yacreader_flow_gl.cpp +// We just reference them here as extern to avoid duplicate symbols + +int YACReaderFlow3D::updateInterval = 16; + +static QShader getShader(const QString &name) +{ + QFile f(name); + return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader(); +} + +/*Constructor*/ +YACReaderFlow3D::YACReaderFlow3D(QWidget *parent, struct Preset p) + : QRhiWidget(parent), numObjects(0), lazyPopulateObjects(-1), hasBeenInitialized(false), backgroundColor(Qt::black), textColor(Qt::white), shadingColor(Qt::black), flowRightToLeft(false), showMarks(true) +{ + updateCount = 0; + config = p; + currentSelected = 0; + + centerPos.x = 0.f; + centerPos.y = 0.f; + centerPos.z = 1.f; + centerPos.rot = 0.f; + + shadingTop = 0.8f; + shadingBottom = 0.02f; + reflectionUp = 0.f; + reflectionBottom = 0.6f; + + setBackgroundColor(Qt::black); + + numObjects = 0; + viewRotate = 0.f; + viewRotateActive = 0; + stepBackup = config.animationStep / config.animationSpeedUp; + + // Request 4x MSAA for the QRhiWidget's render target + setSampleCount(4); + + timerId = -1; +} + +YACReaderFlow3D::~YACReaderFlow3D() +{ + if (timerId != -1) { + killTimer(timerId); + timerId = -1; + } +} + +void YACReaderFlow3D::timerEvent(QTimerEvent *event) +{ + if (timerId == event->timerId()) + update(); +} + +void YACReaderFlow3D::startAnimationTimer() +{ + if (timerId == -1) + timerId = startTimer(updateInterval); +} + +void YACReaderFlow3D::stopAnimationTimer() +{ + if (timerId != -1) { + killTimer(timerId); + timerId = -1; + } +} + +void YACReaderFlow3D::initialize(QRhiCommandBuffer *cb) +{ + if (m_rhi != rhi()) { + releaseResources(); + m_rhi = rhi(); + } + + if (!m_rhi) + return; + + // Initialize default texture from image + if (!defaultTexture) { + QImage defaultImage(":/images/defaultCover.png"); + + defaultTexture = m_rhi->newTexture(QRhiTexture::BGRA8, defaultImage.size(), 1, QRhiTexture::MipMapped); + defaultTexture->create(); + QRhiResourceUpdateBatch *batch = m_rhi->nextResourceUpdateBatch(); + batch->uploadTexture(defaultTexture, defaultImage); + cb->resourceUpdate(batch); + qDebug() << "YACReaderFlow3D: Created defaultTexture" << defaultImage.size(); + } + +#ifdef YACREADER_LIBRARY + // Initialize mark textures + if (!markTexture) { + QImage markImage(":/images/readRibbon.png"); + if (!markImage.isNull()) { + markTexture = m_rhi->newTexture(QRhiTexture::BGRA8, markImage.size(), 1, QRhiTexture::MipMapped); + markTexture->create(); + + QRhiResourceUpdateBatch *batch = m_rhi->nextResourceUpdateBatch(); + batch->uploadTexture(markTexture, markImage); + cb->resourceUpdate(batch); + } + } + + if (!readingTexture) { + QImage readingImage(":/images/readingRibbon.png"); + if (!readingImage.isNull()) { + readingTexture = m_rhi->newTexture(QRhiTexture::BGRA8, readingImage.size(), 1, QRhiTexture::MipMapped); + readingTexture->create(); + + QRhiResourceUpdateBatch *batch = m_rhi->nextResourceUpdateBatch(); + batch->uploadTexture(readingTexture, readingImage); + cb->resourceUpdate(batch); + } + } +#endif + + // Create vertex buffer (quad geometry) + if (!vertexBuffer) { + // Use a triangle list (two triangles = 6 vertices) because some RHI backends + // don't support TriangleFan. Each vertex: x,y,z,u,v (5 floats). + vertexBuffer = m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, 6 * 5 * sizeof(float)); + vertexBuffer->create(); + + // Two triangles forming a quad (triangle list): + // Tri 1: bottom-left, bottom-right, top-right + // Tri 2: bottom-left, top-right, top-left + // Texture coords flipped vertically to match OpenGL convention + float vertices[] = { + // Position (x, y, z), TexCoord (u, v) + -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bottom-left + 0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // Bottom-right + 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // Top-right + + -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bottom-left + 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // Top-right + -0.5f, 0.5f, 0.0f, 0.0f, 0.0f // Top-left + }; + + QRhiResourceUpdateBatch *batch = m_rhi->nextResourceUpdateBatch(); + batch->uploadStaticBuffer(vertexBuffer, vertices); + cb->resourceUpdate(batch); + } + + // Initialize alignment for uniform buffers + if (alignedUniformSize == 0) { + alignedUniformSize = m_rhi->ubufAligned(sizeof(UniformData)); + } + + // Create sampler + if (!sampler) { + // Use no mipmap sampling to avoid LOD changes with camera Z + sampler = m_rhi->newSampler( + QRhiSampler::Linear, + QRhiSampler::Linear, + QRhiSampler::None, + QRhiSampler::ClampToEdge, + QRhiSampler::ClampToEdge); + sampler->create(); + } + + // Create instance buffer for per-draw instance data + if (!instanceBuffer) { + // Allocate buffer for per-instance data (model matrix + shading + opacity + flipFlag) + // mat4 (16 floats) + vec4 (4 floats) + float (1 float) + float (1 float) = 22 floats + instanceBuffer = m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, 22 * sizeof(float)); + instanceBuffer->create(); + } + + // Setup graphics pipeline + if (!pipeline) { + // Load shaders + QShader vertShader = getShader(QLatin1String(":/shaders/flow.vert.qsb")); + QShader fragShader = getShader(QLatin1String(":/shaders/flow.frag.qsb")); + + if (!vertShader.isValid() || !fragShader.isValid()) { + qWarning() << "YACReaderFlow3D: Failed to load shaders!"; + return; + } + + // Create default shader resource bindings for pipeline creation + // We'll create texture-specific ones on-demand in drawCover + shaderBindings = m_rhi->newShaderResourceBindings(); + shaderBindings->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, uniformBuffer), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, defaultTexture, sampler) }); + shaderBindings->create(); + + // Create pipeline + pipeline = m_rhi->newGraphicsPipeline(); + + // Disable alpha blending temporarily to test if blending causes darkening + QRhiGraphicsPipeline::TargetBlend blend; + blend.enable = true; + blend.srcColor = QRhiGraphicsPipeline::SrcAlpha; + blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha; + blend.srcAlpha = QRhiGraphicsPipeline::One; + blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha; + pipeline->setTargetBlends({ blend }); + + // Enable depth test (restore depth writes for normal rendering) + pipeline->setDepthTest(true); + pipeline->setDepthWrite(true); + pipeline->setDepthOp(QRhiGraphicsPipeline::Less); + + // Diagnostic: disable culling to avoid missing-triangle artifacts + pipeline->setCullMode(QRhiGraphicsPipeline::Back); + + // Determine the MSAA sample count to use. Query the RHI for supported counts + // and clamp to at most 4 samples for safety. + int requestedSamples = sampleCount(); + int samplesToUse = 1; + if (requestedSamples > 1 && m_rhi) { + QVector supported = m_rhi->supportedSampleCounts(); + int maxSupported = 1; + for (int s : supported) { + if (s > maxSupported) + maxSupported = s; + } + samplesToUse = qMin(requestedSamples, qMin(4, maxSupported)); + } + if (samplesToUse > 1) + pipeline->setSampleCount(samplesToUse); + + // Use triangle fan topology to match OpenGL draw mode (this makes the app to crash) + // pipeline->setTopology(QRhiGraphicsPipeline::TriangleFan); + + // Set shaders + pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vertShader }, + { QRhiShaderStage::Fragment, fragShader } }); + + // Setup vertex input layout + QRhiVertexInputLayout inputLayout; + inputLayout.setBindings({ + { 5 * sizeof(float) }, // Per-vertex data (position + texCoord) + { 22 * sizeof(float), QRhiVertexInputBinding::PerInstance } // Per-instance data (+ flip flag) + }); + inputLayout.setAttributes({ + // Per-vertex attributes + { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, // position + { 0, 1, QRhiVertexInputAttribute::Float2, 3 * sizeof(float) }, // texCoord + + // Per-instance attributes (model matrix as 4 vec4s) + { 1, 2, QRhiVertexInputAttribute::Float4, 0 * sizeof(float) }, // row 0 + { 1, 3, QRhiVertexInputAttribute::Float4, 4 * sizeof(float) }, // row 1 + { 1, 4, QRhiVertexInputAttribute::Float4, 8 * sizeof(float) }, // row 2 + { 1, 5, QRhiVertexInputAttribute::Float4, 12 * sizeof(float) }, // row 3 + { 1, 6, QRhiVertexInputAttribute::Float4, 16 * sizeof(float) }, // shading vec4 + { 1, 7, QRhiVertexInputAttribute::Float, 20 * sizeof(float) }, // opacity + { 1, 8, QRhiVertexInputAttribute::Float, 21 * sizeof(float) } // flipFlag (1.0 = reflection) + }); + pipeline->setVertexInputLayout(inputLayout); + + // Set shader resource bindings and render pass descriptor + pipeline->setShaderResourceBindings(shaderBindings); + pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor()); + + if (!pipeline->create()) { + qWarning() << "YACReaderFlow3D: Failed to create graphics pipeline!"; + delete pipeline; + pipeline = nullptr; + } + } + + // Call populate only once per data loaded. + if (!hasBeenInitialized && lazyPopulateObjects != -1) { + populate(lazyPopulateObjects); + lazyPopulateObjects = -1; + } + + hasBeenInitialized = true; +} + +void YACReaderFlow3D::ensureUniformBufferCapacity(int requiredSlots) +{ + if (!m_rhi || alignedUniformSize == 0) + return; + + // Check if we need to resize + if (uniformBufferCapacity >= requiredSlots && uniformBuffer) + return; + + // Delete old buffer if it exists + if (uniformBuffer) { + delete uniformBuffer; + uniformBuffer = nullptr; + } + + // Create new larger buffer + // Each draw needs its own uniform slot (cover + reflection + optional mark = 3 per object) + const int totalSize = requiredSlots * alignedUniformSize; + uniformBuffer = m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, totalSize); + if (uniformBuffer->create()) { + uniformBufferCapacity = requiredSlots; + + // Invalidate shader bindings cache since the uniform buffer changed + for (auto *srb : shaderBindingsCache) { + delete srb; + } + shaderBindingsCache.clear(); + + // Recreate default shader bindings for pipeline + if (shaderBindings) { + delete shaderBindings; + } + shaderBindings = m_rhi->newShaderResourceBindings(); + shaderBindings->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, uniformBuffer), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, defaultTexture, sampler) }); + shaderBindings->create(); + } else { + qWarning() << "YACReaderFlow3D: Failed to create uniform buffer of size" << totalSize; + uniformBufferCapacity = 0; + } +} + +void YACReaderFlow3D::render(QRhiCommandBuffer *cb) +{ + if (!m_rhi || numObjects == 0) + return; + + const QSize outputSize = renderTarget()->pixelSize(); + const QColor clearColor = backgroundColor; + + // Update positions and animations + updatePositions(); + + // Prepare view-projection matrix + // Use fixed 20.0 degrees FOV - zoom is controlled via cfZ (camera distance) + QMatrix4x4 projectionMatrix; + projectionMatrix.perspective(20.0, float(outputSize.width()) / float(outputSize.height()), 1.0, 200.0); + + QMatrix4x4 viewMatrix; + viewMatrix.translate(config.cfX, config.cfY, config.cfZ); + viewMatrix.rotate(config.cfRX, 1, 0, 0); + viewMatrix.rotate(viewRotate * config.viewAngle + config.cfRY, 0, 1, 0); + viewMatrix.rotate(config.cfRZ, 0, 0, 1); + + QMatrix4x4 viewProjectionMatrix = projectionMatrix * viewMatrix; + + // Build draw order (back to front for proper alpha blending) + QVector drawOrder; + for (int count = numObjects - 1; count > -1; count--) { + if (count > currentSelected) { + drawOrder.append(count); + } + } + for (int count = 0; count < numObjects - 1; count++) { + if (count < currentSelected) { + drawOrder.append(count); + } + } + drawOrder.append(currentSelected); + + // Structure to hold draw info + struct DrawInfo { + int imageIndex; + bool isReflection; + bool isMark; + QRhiTexture *texture; + float instanceData[22]; + UniformData uniformData; + }; + + // Collect all draws we need to make + // Important: OpenGL draws reflections FIRST, then covers+marks (for correct depth sorting) + QVector draws; + + // Phase 1: Add all reflections + for (int idx : drawOrder) { + if (idx < 0 || idx >= images.size() || !images[idx].texture) + continue; + + DrawInfo reflDraw; + reflDraw.imageIndex = idx; + reflDraw.isReflection = true; + reflDraw.isMark = false; + reflDraw.texture = images[idx].texture; + prepareDrawData(images[idx], true, false, viewProjectionMatrix, reflDraw.instanceData, reflDraw.uniformData); + draws.append(reflDraw); + } + + // Phase 2: Add all covers (and marks) + for (int idx : drawOrder) { + if (idx < 0 || idx >= images.size() || !images[idx].texture) + continue; + + // Add cover draw + DrawInfo coverDraw; + coverDraw.imageIndex = idx; + coverDraw.isReflection = false; + coverDraw.isMark = false; + coverDraw.texture = images[idx].texture; + prepareDrawData(images[idx], false, false, viewProjectionMatrix, coverDraw.instanceData, coverDraw.uniformData); + draws.append(coverDraw); + + if (idx < 0 || idx >= marks.size()) + continue; + + if (idx >= loaded.size()) + continue; + + // Add mark draw immediately after its cover + if (showMarks && loaded[idx] && marks[idx] != Unread) { + QRhiTexture *markTex = (marks[idx] == Read) ? markTexture : readingTexture; + if (markTex) { + DrawInfo markDraw; + markDraw.imageIndex = idx; + markDraw.isReflection = false; + markDraw.isMark = true; + markDraw.texture = markTex; + prepareDrawData(images[idx], false, true, viewProjectionMatrix, markDraw.instanceData, markDraw.uniformData); + draws.append(markDraw); + } + } + } + + // Ensure uniform buffer is large enough + ensureUniformBufferCapacity(draws.size()); + + if (!uniformBuffer) { + qWarning() << "YACReaderFlow3D: No uniform buffer available for rendering"; + return; + } + + // Ensure instance buffer is large enough for all draws + const int requiredInstanceSize = draws.size() * 22 * sizeof(float); + if (!instanceBuffer || instanceBuffer->size() < requiredInstanceSize) { + if (instanceBuffer) { + delete instanceBuffer; + } + instanceBuffer = m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, requiredInstanceSize); + if (!instanceBuffer->create()) { + qWarning() << "YACReaderFlow3D: Failed to create instance buffer of size" << requiredInstanceSize; + return; + } + } + + // === PHASE 1: PREPARE (BEFORE PASS) === + // Update ALL uniform and instance data for ALL draws in one batch + QRhiResourceUpdateBatch *batch = m_rhi->nextResourceUpdateBatch(); + + // Process pending texture uploads + if (!pendingTextureUploads.isEmpty()) { + for (const auto &upload : pendingTextureUploads) { + if (upload.index >= 0 && upload.index < images.size() && images[upload.index].texture) { + batch->uploadTexture(images[upload.index].texture, upload.image); + } + } + pendingTextureUploads.clear(); + } + + // Update uniform buffer with all draw data + for (int i = 0; i < draws.size(); ++i) { + int offset = i * alignedUniformSize; + batch->updateDynamicBuffer(uniformBuffer, offset, sizeof(UniformData), &draws[i].uniformData); + } + + // Update instance buffer with all instance data + for (int i = 0; i < draws.size(); ++i) { + int offset = i * 22 * sizeof(float); + batch->updateDynamicBuffer(instanceBuffer, offset, 22 * sizeof(float), draws[i].instanceData); + } + + // === PHASE 2: RENDER (DURING PASS) === + cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, batch); + + if (pipeline) { + cb->setGraphicsPipeline(pipeline); + cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height())); + + // Execute all draws + for (int i = 0; i < draws.size(); ++i) { + const DrawInfo &draw = draws[i]; + executeDrawWithOffset(cb, draw.texture, draw.instanceData, i); + } + } + + cb->endPass(); +} + +void YACReaderFlow3D::prepareDrawData(const YACReader3DImageRHI &image, bool isReflection, bool isMark, + const QMatrix4x4 &viewProjectionMatrix, + float *outInstanceData, UniformData &outUniformData) +{ + float w = image.width; + float h = image.height; + + // Calculate opacity + float opacity = 1 - 1 / (config.animationFadeOutDist + config.viewRotateLightStrenght * fabs(viewRotate)) * fabs(0 - image.current.x); + + // Calculate shading + float LShading = ((config.rotation != 0) ? ((image.current.rot < 0) ? 1 - 1 / config.rotation * image.current.rot : 1) : 1); + float RShading = ((config.rotation != 0) ? ((image.current.rot > 0) ? 1 - 1 / (config.rotation * -1) * image.current.rot : 1) : 1); + float LUP = shadingTop + (1 - shadingTop) * LShading; + float LDOWN = shadingBottom + (1 - shadingBottom) * LShading; + float RUP = shadingTop + (1 - shadingTop) * RShading; + float RDOWN = shadingBottom + (1 - shadingBottom) * RShading; + + QMatrix4x4 modelMatrix; + modelMatrix.translate(image.current.x, image.current.y, image.current.z); + modelMatrix.rotate(image.current.rot, 0, 1, 0); + + if (isMark) { + // Mark-specific transform + float markWidth = 0.15f; + float markHeight = 0.2f; + float markCenterX = w / 2.0f - 0.125f; + float markCenterY = -0.588f + h; + modelMatrix.translate(markCenterX, markCenterY, 0.001f); + modelMatrix.scale(markWidth, markHeight, 1.0f); + + float shadingValue = RUP * opacity; + outInstanceData[16] = shadingValue; + outInstanceData[17] = shadingValue; + outInstanceData[18] = shadingValue; + outInstanceData[19] = shadingValue; + outInstanceData[20] = 1.0f; + outInstanceData[21] = isReflection ? 1.0f : 0.0f; + } else { + // Cover/reflection transform + if (isReflection) { + modelMatrix.translate(0.0f, -0.5f - h / 2.0f, 0.0f); + // Swap vertical shading for reflection + float temp = LUP; + LUP = LDOWN; + LDOWN = temp; + temp = RUP; + RUP = RDOWN; + RDOWN = temp; + } else { + modelMatrix.translate(0.0f, -0.5f + h / 2.0f, 0.0f); + } + modelMatrix.scale(w, h, 1.0f); + + outInstanceData[16] = LUP; + outInstanceData[17] = LDOWN; + outInstanceData[18] = RUP; + outInstanceData[19] = RDOWN; + outInstanceData[20] = opacity; + outInstanceData[21] = isReflection ? 1.0f : 0.0f; + } + + // Pack model matrix into instance data + const float *matData = modelMatrix.constData(); + for (int i = 0; i < 16; i++) { + outInstanceData[i] = matData[i]; + } + + // Prepare uniform data + outUniformData.viewProjectionMatrix = viewProjectionMatrix; + outUniformData.backgroundColor = QVector3D(backgroundColor.redF(), backgroundColor.greenF(), backgroundColor.blueF()); + outUniformData.shadingColor = QVector3D(shadingColor.redF(), shadingColor.greenF(), shadingColor.blueF()); + outUniformData.reflectionUp = reflectionUp; + outUniformData.reflectionDown = reflectionBottom; + outUniformData.isReflection = isReflection ? 1 : 0; +} + +void YACReaderFlow3D::executeDrawWithOffset(QRhiCommandBuffer *cb, QRhiTexture *texture, + const float *instanceData, int uniformSlot) +{ + if (!texture || !instanceBuffer || !vertexBuffer) + return; + + // NOTE: We cannot update the instance buffer here during the render pass! + // Instead, we'll need to either: + // 1. Use the instance data from uniforms (move it to uniform buffer) + // 2. Or pre-upload all instance data before the pass + // + // For now, let's use approach #1: embed instance data in uniforms via a large instance buffer + // that we populate before the pass, similar to uniforms + // + // Actually, the simplest solution: update the instance buffer ONCE per draw using dynamic updates + // But we need to do this cleverly - we can't call resourceUpdate during pass. + // + // The solution: Create an instance buffer large enough for ALL draws, update it before pass, + // and use offsets during drawing. + + // Get or create shader resource bindings for this texture with dynamic offset support + QRhiShaderResourceBindings *srb = shaderBindingsCache.value(texture, nullptr); + if (!srb) { + srb = m_rhi->newShaderResourceBindings(); + srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, uniformBuffer), + QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture, sampler) }); + srb->create(); + shaderBindingsCache.insert(texture, srb); + } + + // Set shader resources with dynamic offset for uniform buffer + QRhiCommandBuffer::DynamicOffset dynOfs[] = { + { 0, quint32(uniformSlot * alignedUniformSize) } + }; + cb->setShaderResources(srb, 1, dynOfs); + + // Bind vertex buffers with offset into instance buffer + const QRhiCommandBuffer::VertexInput vbufBindings[] = { + { vertexBuffer, 0 }, + { instanceBuffer, quint32(uniformSlot * 22 * sizeof(float)) } // Use slot index for instance data offset + }; + cb->setVertexInput(0, 2, vbufBindings); + + // Draw two triangles (6 vertices) forming a quad + cb->draw(6); +} + +// Note: The old drawCover() and drawMark() methods have been removed. +// Rendering now uses prepareDrawData() and executeDrawWithOffset() which properly +// batch all resource updates before the render pass begins, following Qt RHI best practices. + +void YACReaderFlow3D::releaseResources() +{ + delete vertexBuffer; + vertexBuffer = nullptr; + + delete instanceBuffer; + instanceBuffer = nullptr; + + delete uniformBuffer; + uniformBuffer = nullptr; + + delete sampler; + sampler = nullptr; + + delete pipeline; + pipeline = nullptr; + + delete shaderBindings; + shaderBindings = nullptr; + + // Clean up shader bindings cache + for (auto *srb : shaderBindingsCache) { + delete srb; + } + shaderBindingsCache.clear(); + + delete defaultTexture; + defaultTexture = nullptr; + + delete markTexture; + markTexture = nullptr; + + delete readingTexture; + readingTexture = nullptr; + + m_rhi = nullptr; +} + +void YACReaderFlow3D::showEvent(QShowEvent *event) +{ + QRhiWidget::showEvent(event); + startAnimationTimer(); +} + +void YACReaderFlow3D::cleanupAnimation() +{ + config.animationStep = stepBackup; + viewRotateActive = 0; +} + +void YACReaderFlow3D::draw() +{ + update(); +} + +void YACReaderFlow3D::calcPos(YACReader3DImageRHI &image, int pos) +{ + if (flowRightToLeft) { + pos = pos * -1; + } + + if (pos == 0) { + image.current = centerPos; + } else { + if (pos > 0) { + image.current.x = (config.centerDistance) + (config.xDistance * pos); + image.current.y = config.yDistance * pos * -1; + image.current.z = config.zDistance * pos * -1; + image.current.rot = config.rotation; + } else { + image.current.x = (config.centerDistance) * -1 + (config.xDistance * pos); + image.current.y = config.yDistance * pos; + image.current.z = config.zDistance * pos; + image.current.rot = config.rotation * -1; + } + } +} + +void YACReaderFlow3D::calcVector(YACReader3DVector &vector, int pos) +{ + calcPos(dummy, pos); + vector.x = dummy.current.x; + vector.y = dummy.current.y; + vector.z = dummy.current.z; + vector.rot = dummy.current.rot; +} + +bool YACReaderFlow3D::animate(YACReader3DVector ¤tVector, YACReader3DVector &toVector) +{ + float rotDiff = toVector.rot - currentVector.rot; + float xDiff = toVector.x - currentVector.x; + float yDiff = toVector.y - currentVector.y; + float zDiff = toVector.z - currentVector.z; + + if (fabs(rotDiff) < 0.01 && fabs(xDiff) < 0.001 && fabs(yDiff) < 0.001 && fabs(zDiff) < 0.001) + return true; + + currentVector.x = currentVector.x + (xDiff)*config.animationStep; + currentVector.y = currentVector.y + (yDiff)*config.animationStep; + currentVector.z = currentVector.z + (zDiff)*config.animationStep; + + if (fabs(rotDiff) > 0.01) { + currentVector.rot = currentVector.rot + (rotDiff) * (config.animationStep * config.preRotation); + } else { + viewRotateActive = 0; + } + + return false; +} + +void YACReaderFlow3D::showPrevious() +{ + startAnimationTimer(); + + if (currentSelected > 0) { + currentSelected--; + emit centerIndexChanged(currentSelected); + config.animationStep *= config.animationSpeedUp; + + if (config.animationStep > config.animationStepMax) { + config.animationStep = config.animationStepMax; + } + + if (viewRotateActive && viewRotate > -1) { + viewRotate -= config.viewRotateAdd; + } + + viewRotateActive = 1; + } +} + +void YACReaderFlow3D::showNext() +{ + startAnimationTimer(); + + if (currentSelected < numObjects - 1) { + currentSelected++; + emit centerIndexChanged(currentSelected); + config.animationStep *= config.animationSpeedUp; + + if (config.animationStep > config.animationStepMax) { + config.animationStep = config.animationStepMax; + } + + if (viewRotateActive && viewRotate < 1) { + viewRotate += config.viewRotateAdd; + } + + viewRotateActive = 1; + } +} + +void YACReaderFlow3D::setCurrentIndex(int pos) +{ + if (!(pos >= 0 && pos < images.length() && images.length() > 0)) + return; + if (pos >= images.length() && images.length() > 0) + pos = images.length() - 1; + + startAnimationTimer(); + + currentSelected = pos; + config.animationStep *= config.animationSpeedUp; + + if (config.animationStep > config.animationStepMax) { + config.animationStep = config.animationStepMax; + } + + if (viewRotateActive && viewRotate < 1) { + viewRotate += config.viewRotateAdd; + } + + viewRotateActive = 1; +} + +void YACReaderFlow3D::updatePositions() +{ + int count; + bool stopAnimation = true; + + for (count = numObjects - 1; count > -1; count--) { + calcVector(images[count].animEnd, count - currentSelected); + if (!animate(images[count].current, images[count].animEnd)) + stopAnimation = false; + } + + if (!viewRotateActive) { + viewRotate += (0 - viewRotate) * config.viewRotateSub; + } + + if (fabs(images[currentSelected].current.x - images[currentSelected].animEnd.x) < 1) { + cleanupAnimation(); + if (updateCount >= 0) { + updateCount = 0; + updateImageData(); + } else + updateCount++; + } else + updateCount++; + + if (stopAnimation) + stopAnimationTimer(); +} + +void YACReaderFlow3D::insert(char *name, QRhiTexture *texture, float x, float y, int item) +{ + startAnimationTimer(); + + Q_UNUSED(name) + if (item == -1) { + images.push_back(YACReader3DImageRHI()); + item = numObjects; + numObjects++; + calcVector(images[item].current, item); + images[item].current.z = images[item].current.z - 1; + } + + images[item].texture = texture; + images[item].width = x; + images[item].height = y; + images[item].index = item; +} + +void YACReaderFlow3D::remove(int item) +{ + if (item < 0 || item >= images.size()) + return; + + startAnimationTimer(); + + loaded.remove(item); + marks.remove(item); + + if (item <= currentSelected && currentSelected != 0) { + currentSelected--; + } + + QRhiTexture *texture = images[item].texture; + + int count = item; + while (count <= numObjects - 1) { + images[count].index--; + count++; + } + images.removeAt(item); + + if (texture != defaultTexture) + delete texture; + + numObjects--; +} + +void YACReaderFlow3D::add(int item) +{ + float x = 1; + float y = 1 * (700.f / 480.0f); + QString s = "cover"; + + images.insert(item, YACReader3DImageRHI()); + loaded.insert(item, false); + marks.insert(item, Unread); + numObjects++; + + for (int i = item + 1; i < numObjects; i++) { + images[i].index++; + } + + insert(s.toLocal8Bit().data(), defaultTexture, x, y, item); +} + +YACReader3DImageRHI YACReaderFlow3D::getCurrentSelected() +{ + return images[currentSelected]; +} + +void YACReaderFlow3D::replace(char *name, QRhiTexture *texture, float x, float y, int item) +{ + startAnimationTimer(); + + Q_UNUSED(name) + if (images[item].index == item) { + images[item].texture = texture; + images[item].width = x; + images[item].height = y; + loaded[item] = true; + } else + loaded[item] = false; +} + +void YACReaderFlow3D::populate(int n) +{ + if (hasBeenInitialized) { + clear(); + } + emit centerIndexChanged(0); + + float x = 1; + float y = 1 * (700.f / 480.0f); + int i; + + for (i = 0; i < n; i++) { + QString s = "cover"; + insert(s.toLocal8Bit().data(), defaultTexture, x, y); + } + + loaded = QVector(n, false); +} + +void YACReaderFlow3D::reset() +{ + startAnimationTimer(); + + currentSelected = 0; + loaded.clear(); + + for (int i = 0; i < numObjects; i++) { + if (images[i].texture != defaultTexture) + delete images[i].texture; + } + + numObjects = 0; + images.clear(); + + if (!hasBeenInitialized) + lazyPopulateObjects = -1; +} + +void YACReaderFlow3D::reload() +{ + startAnimationTimer(); + int n = numObjects; + reset(); + populate(n); +} + +// Slot implementations +void YACReaderFlow3D::setCF_RX(int value) +{ + startAnimationTimer(); + config.cfRX = value; +} + +void YACReaderFlow3D::setCF_RY(int value) +{ + startAnimationTimer(); + config.cfRY = value; +} + +void YACReaderFlow3D::setCF_RZ(int value) +{ + startAnimationTimer(); + config.cfRZ = value; +} + +void YACReaderFlow3D::setRotation(int angle) +{ + startAnimationTimer(); + config.rotation = -angle; +} + +void YACReaderFlow3D::setX_Distance(int distance) +{ + startAnimationTimer(); + config.xDistance = distance / 100.0; +} + +void YACReaderFlow3D::setCenter_Distance(int distance) +{ + startAnimationTimer(); + config.centerDistance = distance / 100.0; +} + +void YACReaderFlow3D::setZ_Distance(int distance) +{ + startAnimationTimer(); + config.zDistance = distance / 100.0; +} + +void YACReaderFlow3D::setCF_Y(int value) +{ + startAnimationTimer(); + config.cfY = value / 100.0; +} + +void YACReaderFlow3D::setCF_Z(int value) +{ + startAnimationTimer(); + config.cfZ = value; +} + +void YACReaderFlow3D::setY_Distance(int value) +{ + startAnimationTimer(); + config.yDistance = value / 100.0; +} + +void YACReaderFlow3D::setFadeOutDist(int value) +{ + startAnimationTimer(); + config.animationFadeOutDist = value; +} + +void YACReaderFlow3D::setLightStrenght(int value) +{ + startAnimationTimer(); + config.viewRotateLightStrenght = value; +} + +void YACReaderFlow3D::setMaxAngle(int value) +{ + startAnimationTimer(); + config.viewAngle = value; +} + +void YACReaderFlow3D::setPreset(const Preset &p) +{ + startAnimationTimer(); + config = p; +} + +void YACReaderFlow3D::setZoom(int zoom) +{ + startAnimationTimer(); + config.zoom = zoom; +} + +void YACReaderFlow3D::setPerformance(Performance performance) +{ + if (this->performance != performance) { + startAnimationTimer(); + this->performance = performance; + reload(); + } +} + +void YACReaderFlow3D::useVSync(bool b) +{ + // No-op for RHI - VSync is handled by the platform + Q_UNUSED(b); +} + +void YACReaderFlow3D::setShowMarks(bool value) +{ + startAnimationTimer(); + showMarks = value; +} + +void YACReaderFlow3D::setMarks(QVector marks) +{ + startAnimationTimer(); + this->marks = marks; +} + +void YACReaderFlow3D::setMarkImage(QImage &image) +{ + Q_UNUSED(image); +} + +void YACReaderFlow3D::markSlide(int index, YACReader::YACReaderComicReadStatus status) +{ + startAnimationTimer(); + marks[index] = status; +} + +void YACReaderFlow3D::unmarkSlide(int index) +{ + startAnimationTimer(); + marks[index] = YACReader::Unread; +} + +void YACReaderFlow3D::setSlideSize(QSize size) +{ + Q_UNUSED(size); +} + +void YACReaderFlow3D::clear() +{ + reset(); +} + +void YACReaderFlow3D::setCenterIndex(unsigned int index) +{ + setCurrentIndex(index); +} + +void YACReaderFlow3D::showSlide(int index) +{ + setCurrentIndex(index); +} + +int YACReaderFlow3D::centerIndex() +{ + return currentSelected; +} + +void YACReaderFlow3D::updateMarks() { } + +void YACReaderFlow3D::render() +{ + update(); +} + +void YACReaderFlow3D::resizeGL(int width, int height) +{ + Q_UNUSED(width); + Q_UNUSED(height); + // No-op for RHI - handled automatically +} + +void YACReaderFlow3D::setFlowRightToLeft(bool b) +{ + flowRightToLeft = b; +} + +void YACReaderFlow3D::setBackgroundColor(const QColor &color) +{ + backgroundColor = color; + + // Auto-calculate shadingColor based on background brightness + qreal luminance = (backgroundColor.redF() * 0.299 + + backgroundColor.greenF() * 0.587 + + backgroundColor.blueF() * 0.114); + + if (luminance < 0.5) { + // Dark background - shade towards white + shadingColor = QColor(255, 255, 255); + shadingTop = 0.8f; + shadingBottom = 0.02f; + } else { + // Light background - shade towards black + shadingColor = QColor(0, 0, 0); + shadingTop = 0.95f; + shadingBottom = 0.3f; + } + + update(); +} + +void YACReaderFlow3D::setTextColor(const QColor &color) +{ + textColor = color; + update(); +} + +void YACReaderFlow3D::setShadingColor(const QColor &color) +{ + shadingColor = color; + update(); +} + +// Event handlers +void YACReaderFlow3D::wheelEvent(QWheelEvent *event) +{ + Movement m = getMovement(event); + switch (m) { + case None: + return; + case Forward: + showNext(); + break; + case Backward: + showPrevious(); + break; + default: + break; + } +} + +void YACReaderFlow3D::keyPressEvent(QKeyEvent *event) +{ + if ((event->key() == Qt::Key_Left && !flowRightToLeft) || (event->key() == Qt::Key_Right && flowRightToLeft)) { + if (event->modifiers() == Qt::ControlModifier) + setCurrentIndex((currentSelected - 10 < 0) ? 0 : currentSelected - 10); + else + showPrevious(); + event->accept(); + return; + } + + if ((event->key() == Qt::Key_Right && !flowRightToLeft) || (event->key() == Qt::Key_Left && flowRightToLeft)) { + if (event->modifiers() == Qt::ControlModifier) + setCurrentIndex((currentSelected + 10 >= numObjects) ? numObjects - 1 : currentSelected + 10); + else + showNext(); + event->accept(); + return; + } + + if (event->key() == Qt::Key_Up) { + return; + } + + event->ignore(); +} + +void YACReaderFlow3D::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton && currentSelected >= 0 && currentSelected < images.size()) { + auto position = event->position(); + QVector3D intersection = getPlaneIntersection(position.x(), position.y(), images[currentSelected]); + if ((intersection.x() > 0.5 && !flowRightToLeft) || (intersection.x() < -0.5 && flowRightToLeft)) { + showNext(); + } else if ((intersection.x() < -0.5 && !flowRightToLeft) || (intersection.x() > 0.5 && flowRightToLeft)) { + showPrevious(); + } + } else { + QRhiWidget::mousePressEvent(event); + } +} + +void YACReaderFlow3D::mouseDoubleClickEvent(QMouseEvent *event) +{ + if (currentSelected >= 0 && currentSelected < images.size()) { + auto position = event->position(); + QVector3D intersection = getPlaneIntersection(position.x(), position.y(), images[currentSelected]); + + if (intersection.x() < 0.5 && intersection.x() > -0.5) { + emit selected(centerIndex()); + event->accept(); + } + } +} + +QVector3D YACReaderFlow3D::getPlaneIntersection(int x, int y, YACReader3DImageRHI plane) +{ + // Simplified for now - proper ray-plane intersection calculation needed + // This requires access to the viewport and matrices + const QSize outputSize = renderTarget()->pixelSize(); + + QMatrix4x4 m_projection; + m_projection.perspective(config.zoom, float(outputSize.width()) / float(outputSize.height()), 1.0, 200.0); + + QMatrix4x4 m_modelview; + m_modelview.translate(config.cfX, config.cfY, config.cfZ); + m_modelview.rotate(config.cfRX, 1, 0, 0); + m_modelview.rotate(viewRotate * config.viewAngle + config.cfRY, 0, 1, 0); + m_modelview.rotate(config.cfRZ, 0, 0, 1); + m_modelview.translate(plane.current.x, plane.current.y, plane.current.z); + m_modelview.rotate(plane.current.rot, 0, 1, 0); + m_modelview.scale(plane.width, plane.height, 1.0f); + + QVector3D ray_origin(x * devicePixelRatioF(), y * devicePixelRatioF(), 0); + QVector3D ray_end(x * devicePixelRatioF(), y * devicePixelRatioF(), 1.0); + + ray_origin = ray_origin.unproject(m_modelview, m_projection, QRect(0, 0, outputSize.width(), outputSize.height())); + ray_end = ray_end.unproject(m_modelview, m_projection, QRect(0, 0, outputSize.width(), outputSize.height())); + + QVector3D ray_vector = ray_end - ray_origin; + + QVector3D plane_origin(-0.5f, -0.5f, 0); + QVector3D plane_vektor_1 = QVector3D(0.5f, -0.5f, 0) - plane_origin; + QVector3D plane_vektor_2 = QVector3D(-0.5f, 0.5f, 0) - plane_origin; + + double intersection_LES_determinant = ((plane_vektor_1.x() * plane_vektor_2.y() * (-1) * ray_vector.z()) + + (plane_vektor_2.x() * (-1) * ray_vector.y() * plane_vektor_1.z()) + + ((-1) * ray_vector.x() * plane_vektor_1.y() * plane_vektor_2.z()) - + ((-1) * ray_vector.x() * plane_vektor_2.y() * plane_vektor_1.z()) - + (plane_vektor_1.x() * (-1) * ray_vector.y() * plane_vektor_2.z()) - + (plane_vektor_2.x() * plane_vektor_1.y() * (-1) * ray_vector.z())); + + QVector3D det = ray_origin - plane_origin; + + double intersection_ray_determinant = ((plane_vektor_1.x() * plane_vektor_2.y() * det.z()) + + (plane_vektor_2.x() * det.y() * plane_vektor_1.z()) + + (det.x() * plane_vektor_1.y() * plane_vektor_2.z()) - + (det.x() * plane_vektor_2.y() * plane_vektor_1.z()) - + (plane_vektor_1.x() * det.y() * plane_vektor_2.z()) - + (plane_vektor_2.x() * plane_vektor_1.y() * det.z())); + + return ray_origin + ray_vector * (intersection_ray_determinant / intersection_LES_determinant); +} + +QSize YACReaderFlow3D::minimumSizeHint() const +{ + return QSize(320, 200); +} + +// YACReaderComicFlow3D implementation +YACReaderComicFlow3D::YACReaderComicFlow3D(QWidget *parent, struct Preset p) + : YACReaderFlow3D(parent, p) +{ + worker = new ImageLoader3D(this); + worker->flow = this; +} + +void YACReaderComicFlow3D::setImagePaths(QStringList paths) +{ + worker->reset(); + reset(); + numObjects = 0; + + if (hasBeenInitialized) { + YACReaderFlow3D::populate(paths.size()); + } else { + lazyPopulateObjects = paths.size(); + } + + this->paths = paths; +} + +void YACReaderComicFlow3D::updateImageData() +{ + if (worker->busy()) + return; + + int idx = worker->index(); + if (idx >= 0 && !worker->result().isNull()) { + if (!loaded[idx]) { + float x = 1; + QImage img = worker->result(); + + // // Ensure the loaded image is in RGBA8888 layout so QRhi interprets channels correctly + // if (img.format() != QImage::Format_RGBA8888) + // img = img.convertToFormat(QImage::Format_RGBA8888); + + // Create QRhiTexture from the loaded image + if (m_rhi) { + QRhiTexture *texture = m_rhi->newTexture(QRhiTexture::BGRA8, img.size(), 1, + (performance == high || performance == ultraHigh) ? QRhiTexture::MipMapped : QRhiTexture::UsedAsTransferSource); + + if (texture->create()) { + // Queue texture upload (image already converted to RGBA8888) + PendingTextureUpload upload; + upload.index = idx; + upload.image = img; + upload.x = x; + upload.y = 1 * (float(img.height()) / img.width()); + pendingTextureUploads.append(upload); + + QString s = "cover"; + replace(s.toLocal8Bit().data(), texture, upload.x, upload.y, idx); + } + } + } + } + + int count = 8; + switch (performance) { + case low: + count = 8; + break; + case medium: + count = 10; + break; + case high: + count = 12; + break; + case ultraHigh: + count = 16; + break; + } + + int *indexes = new int[2 * count + 1]; + int center = currentSelected; + indexes[0] = center; + for (int j = 0; j < count; j++) { + indexes[j * 2 + 1] = center + j + 1; + indexes[j * 2 + 2] = center - j - 1; + } + + for (int c = 0; c < 2 * count + 1; c++) { + int i = indexes[c]; + if ((i >= 0) && (i < numObjects)) + if (!loaded[i]) { + if (paths.size() > 0) { + QString fname = paths.at(i); + worker->generate(i, fname); + } + delete[] indexes; + return; + } + } + + delete[] indexes; +} + +void YACReaderComicFlow3D::remove(int item) +{ + worker->lock(); + worker->reset(); + YACReaderFlow3D::remove(item); + if (item >= 0 && item < paths.size()) { + paths.removeAt(item); + } + worker->unlock(); +} + +void YACReaderComicFlow3D::add(const QString &path, int index) +{ + worker->lock(); + worker->reset(); + paths.insert(index, path); + YACReaderFlow3D::add(index); + worker->unlock(); +} + +void YACReaderComicFlow3D::resortCovers(QList newOrder) +{ + worker->lock(); + worker->reset(); + startAnimationTimer(); + + QList pathsNew; + QVector loadedNew; + QVector marksNew; + QVector imagesNew; + + int index = 0; + foreach (int i, newOrder) { + if (i < 0 || i >= images.size()) { + continue; + } + + pathsNew << paths.at(i); + loadedNew << loaded.at(i); + marksNew << marks.at(i); + imagesNew << images.at(i); + imagesNew.last().index = index++; + } + + paths = pathsNew; + loaded = loadedNew; + marks = marksNew; + images = imagesNew; + + worker->unlock(); +} + +// YACReaderPageFlow3D implementation +YACReaderPageFlow3D::YACReaderPageFlow3D(QWidget *parent, struct Preset p) + : YACReaderFlow3D(parent, p) +{ + worker = new ImageLoaderByteArray3D(this); + worker->flow = this; +} + +YACReaderPageFlow3D::~YACReaderPageFlow3D() +{ + if (timerId != -1) { + this->killTimer(timerId); + timerId = -1; + } + rawImages.clear(); + + // Clean up textures + for (auto &image : images) { + if (image.texture != defaultTexture) { + delete image.texture; + } + } +} + +void YACReaderPageFlow3D::updateImageData() +{ + if (worker->busy()) + return; + + int idx = worker->index(); + if (idx >= 0 && !worker->result().isNull()) { + if (!loaded[idx]) { + float x = 1; + QImage img = worker->result(); + + // Create QRhiTexture from the loaded image + if (m_rhi) { + QRhiTexture *texture = m_rhi->newTexture(QRhiTexture::BGRA8, img.size(), 1, + (performance == high || performance == ultraHigh) ? QRhiTexture::MipMapped : QRhiTexture::UsedAsTransferSource); + + if (texture->create()) { + float y = 1 * (float(img.height()) / img.width()); + QString s = "cover"; + replace(s.toLocal8Bit().data(), texture, x, y, idx); + loaded[idx] = true; + } + } + } + } + + int count = 8; + switch (performance) { + case low: + count = 8; + break; + case medium: + count = 10; + break; + case high: + count = 12; + break; + case ultraHigh: + count = 14; + break; + } + + int *indexes = new int[2 * count + 1]; + int center = currentSelected; + indexes[0] = center; + for (int j = 0; j < count; j++) { + indexes[j * 2 + 1] = center + j + 1; + indexes[j * 2 + 2] = center - j - 1; + } + + for (int c = 0; c < 2 * count + 1; c++) { + int i = indexes[c]; + if ((i >= 0) && (i < numObjects)) + if (rawImages.size() > 0) + if (!loaded[i] && imagesReady[i]) { + worker->generate(i, rawImages.at(i)); + delete[] indexes; + return; + } + } + + delete[] indexes; +} + +void YACReaderPageFlow3D::populate(int n) +{ + worker->reset(); + + if (hasBeenInitialized) { + YACReaderFlow3D::populate(n); + } else { + lazyPopulateObjects = n; + } + + imagesReady = QVector(n, false); + rawImages = QVector(n); + imagesSetted = QVector(n, false); +} + +// ImageLoader3D implementation +QImage ImageLoader3D::loadImage(const QString &fileName) +{ + QImage image; + + if (!image.load(fileName)) { + return QImage(); + } + + switch (flow->performance) { + case low: + image = image.scaledToWidth(200, Qt::SmoothTransformation); + break; + case medium: + image = image.scaledToWidth(256, Qt::SmoothTransformation); + break; + case high: + image = image.scaledToWidth(320, Qt::SmoothTransformation); + break; + case ultraHigh: + break; + } + + return image; +} + +ImageLoader3D::ImageLoader3D(YACReaderFlow3D *flow) + : QThread(), flow(flow), restart(false), working(false), idx(-1) +{ +} + +ImageLoader3D::~ImageLoader3D() +{ + mutex.lock(); + condition.wakeOne(); + mutex.unlock(); + wait(); +} + +bool ImageLoader3D::busy() const +{ + return isRunning() ? working : false; +} + +void ImageLoader3D::generate(int index, const QString &fileName) +{ + mutex.lock(); + this->idx = index; + this->fileName = fileName; + this->size = size; + this->img = QImage(); + mutex.unlock(); + + if (!isRunning()) + start(); + else { + restart = true; + condition.wakeOne(); + } +} + +void ImageLoader3D::lock() +{ + mutex.lock(); +} + +void ImageLoader3D::unlock() +{ + mutex.unlock(); +} + +void ImageLoader3D::run() +{ + for (;;) { + mutex.lock(); + this->working = true; + QString fileName = this->fileName; + mutex.unlock(); + + QImage image = loadImage(fileName); + + mutex.lock(); + this->working = false; + this->img = image; + mutex.unlock(); + + mutex.lock(); + if (!this->restart) + condition.wait(&mutex); + restart = false; + mutex.unlock(); + } +} + +QImage ImageLoader3D::result() +{ + return img; +} + +// ImageLoaderByteArray3D implementation +QImage ImageLoaderByteArray3D::loadImage(const QByteArray &raw) +{ + QImage image; + + if (!image.loadFromData(raw)) { + return QImage(); + } + + switch (flow->performance) { + case low: + image = image.scaledToWidth(128, Qt::SmoothTransformation); + break; + case medium: + image = image.scaledToWidth(196, Qt::SmoothTransformation); + break; + case high: + image = image.scaledToWidth(256, Qt::SmoothTransformation); + break; + case ultraHigh: + image = image.scaledToWidth(320, Qt::SmoothTransformation); + break; + } + + return image; +} + +ImageLoaderByteArray3D::ImageLoaderByteArray3D(YACReaderFlow3D *flow) + : QThread(), flow(flow), restart(false), working(false), idx(-1) +{ +} + +ImageLoaderByteArray3D::~ImageLoaderByteArray3D() +{ + mutex.lock(); + condition.wakeOne(); + mutex.unlock(); + wait(); +} + +bool ImageLoaderByteArray3D::busy() const +{ + return isRunning() ? working : false; +} + +void ImageLoaderByteArray3D::generate(int index, const QByteArray &raw) +{ + mutex.lock(); + this->idx = index; + this->rawData = raw; + this->size = size; + this->img = QImage(); + mutex.unlock(); + + if (!isRunning()) + start(); + else { + restart = true; + condition.wakeOne(); + } +} + +void ImageLoaderByteArray3D::run() +{ + for (;;) { + mutex.lock(); + this->working = true; + QByteArray raw = this->rawData; + mutex.unlock(); + + QImage image = loadImage(raw); + + mutex.lock(); + this->working = false; + this->img = image; + mutex.unlock(); + + mutex.lock(); + if (!this->restart) + condition.wait(&mutex); + restart = false; + mutex.unlock(); + } +} + +QImage ImageLoaderByteArray3D::result() +{ + return img; +} diff --git a/common/rhi/yacreader_flow_rhi.h b/common/rhi/yacreader_flow_rhi.h new file mode 100644 index 00000000..7e87769d --- /dev/null +++ b/common/rhi/yacreader_flow_rhi.h @@ -0,0 +1,394 @@ +// Qt RHI-based Coverflow for YACReader +// Compatible with Qt 6.7+ using QRhiWidget +#ifndef __YACREADER_FLOW_RHI_H +#define __YACREADER_FLOW_RHI_H + +#include + +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + +#include +#include +#include +#include + +#include "pictureflow.h" +#include "scroll_management.h" + +// Reuse enums and structs from OpenGL version +enum Performance { + low = 0, + medium, + high, + ultraHigh +}; + +// Cover Vector +struct YACReader3DVector { + float x; + float y; + float z; + float rot; +}; + +// the image/texture info struct +struct YACReader3DImageRHI { + QRhiTexture *texture; + + float width; + float height; + + int index; + + YACReader3DVector current; + YACReader3DVector animEnd; +}; + +struct Preset { + /*** Animation Settings ***/ + float animationStep; + float animationSpeedUp; + float animationStepMax; + float animationFadeOutDist; + float preRotation; + float viewRotateLightStrenght; + float viewRotateAdd; + float viewRotateSub; + float viewAngle; + + /*** Position Configuration ***/ + float cfX; + float cfY; + float cfZ; + float cfRX; + float cfRY; + float cfRZ; + float rotation; + float xDistance; + float centerDistance; + float zDistance; + float yDistance; + + float zoom; +}; + +extern struct Preset defaultYACReaderFlowConfig; +extern struct Preset presetYACReaderFlowClassicConfig; +extern struct Preset presetYACReaderFlowStripeConfig; +extern struct Preset presetYACReaderFlowOverlappedStripeConfig; +extern struct Preset pressetYACReaderFlowUpConfig; +extern struct Preset pressetYACReaderFlowDownConfig; + +class ImageLoader3D; +class ImageLoaderByteArray3D; + +class YACReaderFlow3D : public QRhiWidget, public ScrollManagement +{ + Q_OBJECT + +protected: + int timerId; + + /*** System variables ***/ + YACReader3DImageRHI dummy; + int viewRotateActive; + float stepBackup; + + /*functions*/ + void calcPos(YACReader3DImageRHI &image, int pos); + void calcVector(YACReader3DVector &vector, int pos); + bool animate(YACReader3DVector ¤tVector, YACReader3DVector &toVector); + void prepareInstanceData(const YACReader3DImageRHI &image, bool isReflection, QVector &data); + + int updateCount; + int fontSize; + + // RHI resources + QRhiTexture *defaultTexture = nullptr; + QRhiTexture *markTexture = nullptr; + QRhiTexture *readingTexture = nullptr; + + QRhiBuffer *vertexBuffer = nullptr; + QRhiBuffer *instanceBuffer = nullptr; + QRhiBuffer *uniformBuffer = nullptr; + int alignedUniformSize = 0; // Cached aligned uniform buffer size + int uniformBufferCapacity = 0; // Number of uniform slots allocated + + QRhiSampler *sampler = nullptr; + QRhiGraphicsPipeline *pipeline = nullptr; + QRhiShaderResourceBindings *shaderBindings = nullptr; + + // Cache of shader resource bindings per texture (to avoid recreating every frame) + QMap shaderBindingsCache; + + // Pending texture uploads (for async image loading) + struct PendingTextureUpload { + int index; + QImage image; + float x; + float y; + }; + QVector pendingTextureUploads; + + // Uniform buffer data structure + struct UniformData { + QMatrix4x4 viewProjectionMatrix; + QVector3D backgroundColor; + float _pad0; + QVector3D shadingColor; + float _pad1; + float reflectionUp; + float reflectionDown; + int isReflection; + float _pad2; + }; + + void timerEvent(QTimerEvent *); + + int numObjects; + int lazyPopulateObjects; + bool showMarks; + QVector loaded; + QVector marks; + + QVector images; + + bool hasBeenInitialized; + + Performance performance; + + /*** Animation Settings ***/ + Preset config; + + int currentSelected; + + YACReader3DVector centerPos; + + /*** Style ***/ + float shadingTop; + float shadingBottom; + + float reflectionUp; + float reflectionBottom; + + /*** Theme Colors ***/ + QColor backgroundColor; + QColor textColor; + QColor shadingColor; + + /*** System info ***/ + float viewRotate; + + static int updateInterval; + + bool flowRightToLeft; + + void startAnimationTimer(); + void stopAnimationTimer(); + + // QRhiWidget overrides + void initialize(QRhiCommandBuffer *cb) override; + void render(QRhiCommandBuffer *cb) override; + void releaseResources() override; + void showEvent(QShowEvent *event) override; + + // Helper methods + QRhiTexture *createTextureFromImage(QRhiCommandBuffer *cb, const QImage &image); + void updateUniformBuffer(QRhiCommandBuffer *cb, const UniformData &data); + void prepareMarkInstanceData(const YACReader3DImageRHI &image, QVector &data); + void ensureUniformBufferCapacity(int requiredSlots); + void prepareDrawData(const YACReader3DImageRHI &image, bool isReflection, bool isMark, + const QMatrix4x4 &viewProjectionMatrix, float *outInstanceData, + UniformData &outUniformData); + void executeDrawWithOffset(QRhiCommandBuffer *cb, QRhiTexture *texture, + const float *instanceData, int uniformSlot); + +protected: + QRhi *m_rhi = nullptr; + std::unique_ptr m_vbuf; + std::unique_ptr m_ubuf; + std::unique_ptr m_srb; + std::unique_ptr m_pipeline; + QMatrix4x4 m_viewProjection; + float m_rotation = 0.0f; + +public: + YACReaderFlow3D(QWidget *parent = nullptr, struct Preset p = pressetYACReaderFlowDownConfig); + virtual ~YACReaderFlow3D(); + + QSize minimumSizeHint() const override; + + void showPrevious(); + void showNext(); + void setCurrentIndex(int pos); + void cleanupAnimation(); + void draw(); + void updatePositions(); + void insert(char *name, QRhiTexture *texture, float x, float y, int item = -1); + virtual void remove(int item); + void add(int item); + void replace(char *name, QRhiTexture *texture, float x, float y, int item); + void populate(int n); + YACReader3DImageRHI getCurrentSelected(); + +public slots: + void setCF_RX(int value); + void setCF_RY(int value); + void setCF_RZ(int value); + void setZoom(int zoom); + void setRotation(int angle); + void setX_Distance(int distance); + void setCenter_Distance(int distance); + void setZ_Distance(int distance); + void setCF_Y(int value); + void setCF_Z(int value); + void setY_Distance(int value); + void setFadeOutDist(int value); + void setLightStrenght(int value); + void setMaxAngle(int value); + void setPreset(const Preset &p); + void setPerformance(Performance performance); + void useVSync(bool b); // Compatibility method (no-op for RHI) + void setFlowRightToLeft(bool b); + + // Theme color setters + void setBackgroundColor(const QColor &color); + void setTextColor(const QColor &color); + void setShadingColor(const QColor &color); + + virtual void updateImageData() = 0; + + void reset(); + void reload(); + + void setShowMarks(bool value); + void setMarks(QVector marks); + void setMarkImage(QImage &image); + void markSlide(int index, YACReader::YACReaderComicReadStatus status); + void unmarkSlide(int index); + void setSlideSize(QSize size); + void clear(); + void setCenterIndex(unsigned int index); + void showSlide(int index); + int centerIndex(); + void updateMarks(); + void render(); // Compatibility method (triggers update()) + void resizeGL(int width, int height); // Compatibility method (no-op for RHI) + + QVector3D getPlaneIntersection(int x, int y, YACReader3DImageRHI plane); + void mouseDoubleClickEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + + friend class ImageLoader3D; + friend class ImageLoaderByteArray3D; + +signals: + void centerIndexChanged(int); + void selected(unsigned int); +}; + +class YACReaderComicFlow3D : public YACReaderFlow3D +{ +public: + YACReaderComicFlow3D(QWidget *parent = nullptr, struct Preset p = defaultYACReaderFlowConfig); + void setImagePaths(QStringList paths); + void updateImageData() override; + void remove(int item) override; + void add(const QString &path, int index); + void resortCovers(QList newOrder); + friend class ImageLoader3D; + +private: + ImageLoader3D *worker; + +protected: + QList paths; +}; + +class YACReaderPageFlow3D : public YACReaderFlow3D +{ +public: + YACReaderPageFlow3D(QWidget *parent = nullptr, struct Preset p = defaultYACReaderFlowConfig); + ~YACReaderPageFlow3D(); + void updateImageData() override; + void populate(int n); + QVector imagesReady; + QVector rawImages; + QVector imagesSetted; + friend class ImageLoaderByteArray3D; + +private: + ImageLoaderByteArray3D *worker; +}; + +class ImageLoader3D : public QThread +{ +public: + ImageLoader3D(YACReaderFlow3D *flow); + ~ImageLoader3D(); + bool busy() const; + void generate(int index, const QString &fileName); + void reset() + { + idx = -1; + fileName = ""; + } + int index() const { return idx; } + void lock(); + void unlock(); + QImage result(); + YACReaderFlow3D *flow; + QImage loadImage(const QString &fileName); + +protected: + void run() override; + +private: + QMutex mutex; + QWaitCondition condition; + + bool restart; + bool working; + int idx; + QString fileName; + QSize size; + QImage img; +}; + +class ImageLoaderByteArray3D : public QThread +{ +public: + ImageLoaderByteArray3D(YACReaderFlow3D *flow); + ~ImageLoaderByteArray3D(); + bool busy() const; + void generate(int index, const QByteArray &raw); + void reset() + { + idx = -1; + rawData.clear(); + } + int index() const { return idx; } + QImage result(); + YACReaderFlow3D *flow; + QImage loadImage(const QByteArray &rawData); + +protected: + void run() override; + +private: + QMutex mutex; + QWaitCondition condition; + + bool restart; + bool working; + int idx; + QByteArray rawData; + QSize size; + QImage img; +}; + +#endif // QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + +#endif // __YACREADER_FLOW_RHI_H diff --git a/third_party/QsLog/QsLogDestConsole.cpp b/third_party/QsLog/QsLogDestConsole.cpp index 472d538d..d7d21646 100644 --- a/third_party/QsLog/QsLogDestConsole.cpp +++ b/third_party/QsLog/QsLogDestConsole.cpp @@ -41,6 +41,9 @@ void QsDebugOutput::output( const QString& message ) { WriteConsoleW(GetStdHandle(STD_ERROR_HANDLE), message.utf16(), message.size(), NULL, NULL); WriteConsoleW(GetStdHandle(STD_ERROR_HANDLE), L"\n", 1, NULL, NULL); + + fprintf(stdout, "%s\n", qPrintable(message)); + fflush(stdout); } #endif