mirror of
https://github.com/lubosz/overte.git
synced 2025-04-06 05:42:19 +02:00
Working on mac GL issues
This commit is contained in:
parent
7339a43536
commit
95d160a170
28 changed files with 706 additions and 355 deletions
|
@ -66,7 +66,7 @@ if (ANDROID)
|
|||
set(GLES_OPTION ON)
|
||||
set(PLATFORM_QT_COMPONENTS AndroidExtras WebView)
|
||||
else ()
|
||||
set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets)
|
||||
set(PLATFORM_QT_COMPONENTS WebEngine)
|
||||
endif ()
|
||||
|
||||
if (USE_GLES AND (NOT ANDROID))
|
||||
|
|
|
@ -52,6 +52,8 @@
|
|||
#include <QTemporaryDir>
|
||||
|
||||
#include <gl/QOpenGLContextWrapper.h>
|
||||
#include <gl/GLWindow.h>
|
||||
#include <gl/GLHelpers.h>
|
||||
|
||||
#include <shared/FileUtils.h>
|
||||
#include <shared/QtHelpers.h>
|
||||
|
@ -971,9 +973,11 @@ OffscreenGLCanvas* _qmlShareContext { nullptr };
|
|||
// and manually set THAT to be the shared context for the Chromium helper
|
||||
#if !defined(DISABLE_QML)
|
||||
OffscreenGLCanvas* _chromiumShareContext { nullptr };
|
||||
Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context);
|
||||
#endif
|
||||
|
||||
Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context);
|
||||
Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context();
|
||||
|
||||
Setting::Handle<int> sessionRunTime{ "sessionRunTime", 0 };
|
||||
|
||||
const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f;
|
||||
|
@ -1370,7 +1374,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
_glWidget->setMouseTracking(true);
|
||||
// Make sure the window is set to the correct size by processing the pending events
|
||||
QCoreApplication::processEvents();
|
||||
_glWidget->createContext();
|
||||
|
||||
// Create the main thread context, the GPU backend
|
||||
initializeGL();
|
||||
|
@ -2727,46 +2730,58 @@ void Application::initializeGL() {
|
|||
_isGLInitialized = true;
|
||||
}
|
||||
|
||||
_glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat());
|
||||
|
||||
// When loading QtWebEngineWidgets, it creates a global share context on startup.
|
||||
// We have to account for this possibility by checking here for an existing
|
||||
// global share context
|
||||
auto globalShareContext = qt_gl_global_share_context();
|
||||
_glWidget->createContext(globalShareContext);
|
||||
|
||||
if (!_glWidget->makeCurrent()) {
|
||||
qCWarning(interfaceapp, "Unable to make window context current");
|
||||
}
|
||||
|
||||
#if !defined(DISABLE_QML)
|
||||
// Build a shared canvas / context for the Chromium processes
|
||||
{
|
||||
// Disable signed distance field font rendering on ATI/AMD GPUs, due to
|
||||
// https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app
|
||||
std::string vendor{ (const char*)glGetString(GL_VENDOR) };
|
||||
if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) {
|
||||
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text"));
|
||||
}
|
||||
// Disable signed distance field font rendering on ATI/AMD GPUs, due to
|
||||
// https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app
|
||||
std::string vendor{ (const char*)glGetString(GL_VENDOR) };
|
||||
if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) {
|
||||
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text"));
|
||||
}
|
||||
|
||||
// Build a shared canvas / context for the Chromium processes
|
||||
if (!globalShareContext) {
|
||||
// Chromium rendering uses some GL functions that prevent nSight from capturing
|
||||
// frames, so we only create the shared context if nsight is NOT active.
|
||||
if (!nsightActive()) {
|
||||
_chromiumShareContext = new OffscreenGLCanvas();
|
||||
_chromiumShareContext = new OffscreenGLCanvas();
|
||||
_chromiumShareContext->setObjectName("ChromiumShareContext");
|
||||
_chromiumShareContext->create(_glWidget->qglContext());
|
||||
if (!_chromiumShareContext->makeCurrent()) {
|
||||
qCWarning(interfaceapp, "Unable to make chromium shared context current");
|
||||
}
|
||||
qt_gl_set_global_share_context(_chromiumShareContext->getContext());
|
||||
globalShareContext = _chromiumShareContext->getContext();
|
||||
qt_gl_set_global_share_context(globalShareContext);
|
||||
_chromiumShareContext->doneCurrent();
|
||||
// Restore the GL widget context
|
||||
if (!_glWidget->makeCurrent()) {
|
||||
qCWarning(interfaceapp, "Unable to make window context current");
|
||||
}
|
||||
} else {
|
||||
qCWarning(interfaceapp) << "nSight detected, disabling chrome rendering";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!globalShareContext) {
|
||||
globalShareContext = _glWidget->qglContext();
|
||||
qt_gl_set_global_share_context(globalShareContext);
|
||||
}
|
||||
|
||||
// Build a shared canvas / context for the QML rendering
|
||||
{
|
||||
_qmlShareContext = new OffscreenGLCanvas();
|
||||
_qmlShareContext->setObjectName("QmlShareContext");
|
||||
_qmlShareContext->create(_glWidget->qglContext());
|
||||
_qmlShareContext->create(globalShareContext);
|
||||
if (!_qmlShareContext->makeCurrent()) {
|
||||
qCWarning(interfaceapp, "Unable to make QML shared context current");
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include <SandboxUtils.h>
|
||||
#include <SharedUtil.h>
|
||||
#include <NetworkAccessManager.h>
|
||||
#include <gl/GLHelpers.h>
|
||||
|
||||
#include "AddressManager.h"
|
||||
#include "Application.h"
|
||||
|
@ -40,6 +41,18 @@ extern "C" {
|
|||
#endif
|
||||
|
||||
int main(int argc, const char* argv[]) {
|
||||
auto format = getDefaultOpenGLSurfaceFormat();
|
||||
#ifdef Q_OS_MAC
|
||||
// Deal with some weirdness in the chromium context sharing on Mac.
|
||||
// The primary share context needs to be 3.2, so that the Chromium will
|
||||
// succeed in it's creation of it's command stub contexts.
|
||||
format.setVersion(3, 2);
|
||||
// This appears to resolve the issues with corrupted fonts on OSX. No
|
||||
// idea why.
|
||||
qputenv("QT_ENABLE_GLYPH_CACHE_WORKAROUND", "true");
|
||||
// https://i.kym-cdn.com/entries/icons/original/000/008/342/ihave.jpg
|
||||
#endif
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
setupHifiApplication(BuildInfo::INTERFACE_NAME);
|
||||
|
||||
QStringList arguments;
|
||||
|
|
|
@ -115,6 +115,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) {
|
|||
batch.resetViewTransform();
|
||||
batch.setResourceTexture(0, _uiTexture);
|
||||
geometryCache->renderUnitQuad(batch, glm::vec4(1), _qmlGeometryId);
|
||||
batch.setResourceTexture(0, nullptr);
|
||||
}
|
||||
|
||||
void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) {
|
||||
|
|
|
@ -261,6 +261,7 @@ void WebEntityRenderer::doRender(RenderArgs* args) {
|
|||
DependencyManager::get<GeometryCache>()->bindWebBrowserProgram(batch, fadeRatio < OPAQUE_ALPHA_THRESHOLD);
|
||||
DependencyManager::get<GeometryCache>()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio), _geometryId);
|
||||
batch.popProjectionJitter();
|
||||
batch.setResourceTexture(0, nullptr);
|
||||
}
|
||||
|
||||
bool WebEntityRenderer::hasWebSurface() {
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include "GLLogging.h"
|
||||
#include "Config.h"
|
||||
#include "GLHelpers.h"
|
||||
#include "QOpenGLContextWrapper.h"
|
||||
|
||||
using namespace gl;
|
||||
|
||||
|
@ -68,8 +69,6 @@ void Context::updateSwapchainMemoryUsage(size_t prevSize, size_t newSize) {
|
|||
}
|
||||
|
||||
|
||||
Context* Context::PRIMARY = nullptr;
|
||||
|
||||
Context::Context() {}
|
||||
|
||||
Context::Context(QWindow* window) {
|
||||
|
@ -97,9 +96,6 @@ void Context::release() {
|
|||
_context = nullptr;
|
||||
#endif
|
||||
_window = nullptr;
|
||||
if (PRIMARY == this) {
|
||||
PRIMARY = nullptr;
|
||||
}
|
||||
updateSwapchainMemoryCounter();
|
||||
}
|
||||
|
||||
|
@ -235,16 +231,10 @@ typedef HGLRC(APIENTRYP PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hDC, HGLRC hShare
|
|||
GLAPI PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB;
|
||||
GLAPI PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB;
|
||||
|
||||
Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context();
|
||||
|
||||
void Context::create() {
|
||||
if (!PRIMARY) {
|
||||
PRIMARY = static_cast<Context*>(qApp->property(hifi::properties::gl::PRIMARY_CONTEXT).value<void*>());
|
||||
}
|
||||
|
||||
if (PRIMARY) {
|
||||
_version = PRIMARY->_version;
|
||||
}
|
||||
|
||||
void Context::create(QOpenGLContext* shareContext) {
|
||||
assert(0 != _hwnd);
|
||||
assert(0 == _hdc);
|
||||
auto hwnd = _hwnd;
|
||||
|
@ -338,7 +328,10 @@ void Context::create() {
|
|||
contextAttribs.push_back(0);
|
||||
}
|
||||
contextAttribs.push_back(0);
|
||||
auto shareHglrc = PRIMARY ? PRIMARY->_hglrc : 0;
|
||||
if (!shareContext) {
|
||||
shareContext = qt_gl_global_share_context();
|
||||
}
|
||||
HGLRC shareHglrc = (HGLRC)QOpenGLContextWrapper::nativeContext(shareContext);
|
||||
_hglrc = wglCreateContextAttribsARB(_hdc, shareHglrc, &contextAttribs[0]);
|
||||
}
|
||||
|
||||
|
@ -346,11 +339,6 @@ void Context::create() {
|
|||
throw std::runtime_error("Could not create GL context");
|
||||
}
|
||||
|
||||
if (!PRIMARY) {
|
||||
PRIMARY = this;
|
||||
qApp->setProperty(hifi::properties::gl::PRIMARY_CONTEXT, QVariant::fromValue((void*)PRIMARY));
|
||||
}
|
||||
|
||||
if (!makeCurrent()) {
|
||||
throw std::runtime_error("Could not make context current");
|
||||
}
|
||||
|
@ -368,7 +356,7 @@ OffscreenContext::~OffscreenContext() {
|
|||
_window->deleteLater();
|
||||
}
|
||||
|
||||
void OffscreenContext::create() {
|
||||
void OffscreenContext::create(QOpenGLContext* shareContext) {
|
||||
if (!_window) {
|
||||
_window = new QWindow();
|
||||
_window->setFlags(Qt::MSWindowsOwnDC);
|
||||
|
@ -379,5 +367,5 @@ void OffscreenContext::create() {
|
|||
qCDebug(glLogging) << "New Offscreen GLContext, window size = " << windowSize.width() << " , " << windowSize.height();
|
||||
QGuiApplication::processEvents();
|
||||
}
|
||||
Parent::create();
|
||||
Parent::create(shareContext);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ class QSurface;
|
|||
class QWindow;
|
||||
class QOpenGLContext;
|
||||
class QThread;
|
||||
class QOpenGLDebugMessage;
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#define GL_CUSTOM_CONTEXT
|
||||
|
@ -30,7 +31,6 @@ namespace gl {
|
|||
class Context {
|
||||
protected:
|
||||
QWindow* _window { nullptr };
|
||||
static Context* PRIMARY;
|
||||
static void destroyContext(QOpenGLContext* context);
|
||||
#if defined(GL_CUSTOM_CONTEXT)
|
||||
uint32_t _version { 0x0401 };
|
||||
|
@ -48,6 +48,9 @@ namespace gl {
|
|||
|
||||
public:
|
||||
static bool enableDebugLogger();
|
||||
static void debugMessageHandler(const QOpenGLDebugMessage &debugMessage);
|
||||
static void setupDebugLogging(QOpenGLContext* context);
|
||||
|
||||
Context();
|
||||
Context(QWindow* window);
|
||||
void release();
|
||||
|
@ -59,14 +62,14 @@ namespace gl {
|
|||
static void makeCurrent(QOpenGLContext* context, QSurface* surface);
|
||||
void swapBuffers();
|
||||
void doneCurrent();
|
||||
virtual void create();
|
||||
virtual void create(QOpenGLContext* shareContext = nullptr);
|
||||
QOpenGLContext* qglContext();
|
||||
void moveToThread(QThread* thread);
|
||||
|
||||
static size_t evalSurfaceMemoryUsage(uint32_t width, uint32_t height, uint32_t pixelSize);
|
||||
static size_t getSwapchainMemoryUsage();
|
||||
static void updateSwapchainMemoryUsage(size_t prevSize, size_t newSize);
|
||||
|
||||
|
||||
private:
|
||||
static std::atomic<size_t> _totalSwapchainMemoryUsage;
|
||||
|
||||
|
@ -81,7 +84,7 @@ namespace gl {
|
|||
QWindow* _window { nullptr };
|
||||
public:
|
||||
virtual ~OffscreenContext();
|
||||
void create() override;
|
||||
void create(QOpenGLContext* shareContext = nullptr) override;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
#include <QtPlatformHeaders/QWGLNativeContext>
|
||||
#endif
|
||||
|
||||
#include <QtGui/QOpenGLDebugMessage>
|
||||
|
||||
#include "GLHelpers.h"
|
||||
|
||||
using namespace gl;
|
||||
|
@ -47,6 +49,32 @@ void Context::moveToThread(QThread* thread) {
|
|||
qglContext()->moveToThread(thread);
|
||||
}
|
||||
|
||||
void Context::debugMessageHandler(const QOpenGLDebugMessage& debugMessage) {
|
||||
auto severity = debugMessage.severity();
|
||||
switch (severity) {
|
||||
case QOpenGLDebugMessage::NotificationSeverity:
|
||||
case QOpenGLDebugMessage::LowSeverity:
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
qDebug(glLogging) << debugMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
void Context::setupDebugLogging(QOpenGLContext *context) {
|
||||
QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(context);
|
||||
QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, nullptr, [](const QOpenGLDebugMessage& message){
|
||||
Context::debugMessageHandler(message);
|
||||
});
|
||||
if (logger->initialize()) {
|
||||
logger->enableMessages();
|
||||
logger->startLogging(QOpenGLDebugLogger::SynchronousLogging);
|
||||
} else {
|
||||
qCWarning(glLogging) << "OpenGL context does not support debugging";
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef GL_CUSTOM_CONTEXT
|
||||
bool Context::makeCurrent() {
|
||||
updateSwapchainMemoryCounter();
|
||||
|
@ -65,21 +93,29 @@ void Context::doneCurrent() {
|
|||
}
|
||||
}
|
||||
|
||||
Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context();
|
||||
const QSurfaceFormat& getDefaultOpenGLSurfaceFormat();
|
||||
|
||||
|
||||
void Context::create() {
|
||||
void Context::create(QOpenGLContext* shareContext) {
|
||||
_context = new QOpenGLContext();
|
||||
if (PRIMARY) {
|
||||
_context->setShareContext(PRIMARY->qglContext());
|
||||
} else {
|
||||
PRIMARY = this;
|
||||
_context->setFormat(_window->format());
|
||||
if (!shareContext) {
|
||||
shareContext = qt_gl_global_share_context();
|
||||
}
|
||||
_context->setFormat(getDefaultOpenGLSurfaceFormat());
|
||||
_context->create();
|
||||
|
||||
_context->setShareContext(shareContext);
|
||||
_context->create();
|
||||
_swapchainPixelSize = evalGLFormatSwapchainPixelSize(_context->format());
|
||||
updateSwapchainMemoryCounter();
|
||||
|
||||
if (!makeCurrent()) {
|
||||
throw std::runtime_error("Could not make context current");
|
||||
}
|
||||
if (enableDebugLogger()) {
|
||||
setupDebugLogging(_context);
|
||||
}
|
||||
doneCurrent();
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -38,10 +38,15 @@ void gl::getTargetVersion(int& major, int& minor) {
|
|||
#if defined(USE_GLES)
|
||||
major = 3;
|
||||
minor = 2;
|
||||
#else
|
||||
#if defined(Q_OS_MAC)
|
||||
major = 4;
|
||||
minor = 1;
|
||||
#else
|
||||
major = 4;
|
||||
minor = disableGl45() ? 1 : 5;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() {
|
||||
|
@ -57,6 +62,7 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() {
|
|||
#else
|
||||
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
|
||||
#endif
|
||||
format.setOption(QSurfaceFormat::DebugContext);
|
||||
// Qt Quick may need a depth and stencil buffer. Always make sure these are available.
|
||||
format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS);
|
||||
format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS);
|
||||
|
@ -64,7 +70,6 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() {
|
|||
::gl::getTargetVersion(major, minor);
|
||||
format.setMajorVersion(major);
|
||||
format.setMinorVersion(minor);
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
});
|
||||
return format;
|
||||
}
|
||||
|
|
|
@ -63,10 +63,10 @@ int GLWidget::getDeviceHeight() const {
|
|||
return height() * (windowHandle() ? (float)windowHandle()->devicePixelRatio() : 1.0f);
|
||||
}
|
||||
|
||||
void GLWidget::createContext() {
|
||||
void GLWidget::createContext(QOpenGLContext* shareContext) {
|
||||
_context = new gl::Context();
|
||||
_context->setWindow(windowHandle());
|
||||
_context->create();
|
||||
_context->create(shareContext);
|
||||
_context->makeCurrent();
|
||||
_context->clear();
|
||||
_context->doneCurrent();
|
||||
|
|
|
@ -29,7 +29,7 @@ public:
|
|||
int getDeviceHeight() const;
|
||||
QSize getDeviceSize() const { return QSize(getDeviceWidth(), getDeviceHeight()); }
|
||||
QPaintEngine* paintEngine() const override;
|
||||
void createContext();
|
||||
void createContext(QOpenGLContext* shareContext = nullptr);
|
||||
bool makeCurrent();
|
||||
void doneCurrent();
|
||||
void swapBuffers();
|
||||
|
|
|
@ -22,7 +22,7 @@ void GLWindow::createContext(QOpenGLContext* shareContext) {
|
|||
void GLWindow::createContext(const QSurfaceFormat& format, QOpenGLContext* shareContext) {
|
||||
_context = new gl::Context();
|
||||
_context->setWindow(this);
|
||||
_context->create();
|
||||
_context->create(shareContext);
|
||||
_context->makeCurrent();
|
||||
_context->clear();
|
||||
}
|
||||
|
|
|
@ -75,32 +75,9 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) {
|
|||
}
|
||||
#endif
|
||||
|
||||
if (gl::Context::enableDebugLogger()) {
|
||||
_context->makeCurrent(_offscreenSurface);
|
||||
QOpenGLDebugLogger *logger = new QOpenGLDebugLogger(this);
|
||||
connect(logger, &QOpenGLDebugLogger::messageLogged, this, &OffscreenGLCanvas::onMessageLogged);
|
||||
logger->initialize();
|
||||
logger->enableMessages();
|
||||
logger->startLogging(QOpenGLDebugLogger::SynchronousLogging);
|
||||
_context->doneCurrent();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OffscreenGLCanvas::onMessageLogged(const QOpenGLDebugMessage& debugMessage) {
|
||||
auto severity = debugMessage.severity();
|
||||
switch (severity) {
|
||||
case QOpenGLDebugMessage::NotificationSeverity:
|
||||
case QOpenGLDebugMessage::LowSeverity:
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
qDebug(glLogging) << debugMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
bool OffscreenGLCanvas::makeCurrent() {
|
||||
bool result = _context->makeCurrent(_offscreenSurface);
|
||||
if (glGetString) {
|
||||
|
|
|
@ -35,9 +35,6 @@ public:
|
|||
void setThreadContext();
|
||||
static bool restoreThreadContext();
|
||||
|
||||
private slots:
|
||||
void onMessageLogged(const QOpenGLDebugMessage &debugMessage);
|
||||
|
||||
protected:
|
||||
void clearThreadContext();
|
||||
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
|
||||
#include <QOpenGLContext>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <QtPlatformHeaders/QWGLNativeContext>
|
||||
#endif
|
||||
|
||||
uint32_t QOpenGLContextWrapper::currentContextVersion() {
|
||||
QOpenGLContext* context = QOpenGLContext::currentContext();
|
||||
if (!context) {
|
||||
|
@ -45,6 +49,19 @@ void QOpenGLContextWrapper::setFormat(const QSurfaceFormat& format) {
|
|||
_context->setFormat(format);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
void* QOpenGLContextWrapper::nativeContext(QOpenGLContext* context) {
|
||||
HGLRC result = 0;
|
||||
if (context != nullptr) {
|
||||
auto nativeHandle = context->nativeHandle();
|
||||
if (nativeHandle.canConvert<QWGLNativeContext>()) {
|
||||
result = nativeHandle.value<QWGLNativeContext>().context();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool QOpenGLContextWrapper::create() {
|
||||
return _context->create();
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#define hifi_QOpenGLContextWrapper_h
|
||||
|
||||
#include <stdint.h>
|
||||
#include <QtGlobal>
|
||||
|
||||
class QOpenGLContext;
|
||||
class QSurface;
|
||||
|
@ -21,6 +22,10 @@ class QThread;
|
|||
|
||||
class QOpenGLContextWrapper {
|
||||
public:
|
||||
#ifdef Q_OS_WIN
|
||||
static void* nativeContext(QOpenGLContext* context);
|
||||
#endif
|
||||
|
||||
QOpenGLContextWrapper();
|
||||
QOpenGLContextWrapper(QOpenGLContext* context);
|
||||
virtual ~QOpenGLContextWrapper();
|
||||
|
|
|
@ -708,37 +708,37 @@ void GLBackend::do_glColor4f(const Batch& batch, size_t paramOffset) {
|
|||
|
||||
void GLBackend::releaseBuffer(GLuint id, Size size) const {
|
||||
Lock lock(_trashMutex);
|
||||
_buffersTrash.push_back({ id, size });
|
||||
_currentFrameTrash.buffersTrash.push_back({ id, size });
|
||||
}
|
||||
|
||||
void GLBackend::releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const {
|
||||
Lock lock(_trashMutex);
|
||||
_externalTexturesTrash.push_back({ id, recycler });
|
||||
_currentFrameTrash.externalTexturesTrash.push_back({ id, recycler });
|
||||
}
|
||||
|
||||
void GLBackend::releaseTexture(GLuint id, Size size) const {
|
||||
Lock lock(_trashMutex);
|
||||
_texturesTrash.push_back({ id, size });
|
||||
_currentFrameTrash.texturesTrash.push_back({ id, size });
|
||||
}
|
||||
|
||||
void GLBackend::releaseFramebuffer(GLuint id) const {
|
||||
Lock lock(_trashMutex);
|
||||
_framebuffersTrash.push_back(id);
|
||||
_currentFrameTrash.framebuffersTrash.push_back(id);
|
||||
}
|
||||
|
||||
void GLBackend::releaseShader(GLuint id) const {
|
||||
Lock lock(_trashMutex);
|
||||
_shadersTrash.push_back(id);
|
||||
_currentFrameTrash.shadersTrash.push_back(id);
|
||||
}
|
||||
|
||||
void GLBackend::releaseProgram(GLuint id) const {
|
||||
Lock lock(_trashMutex);
|
||||
_programsTrash.push_back(id);
|
||||
_currentFrameTrash.programsTrash.push_back(id);
|
||||
}
|
||||
|
||||
void GLBackend::releaseQuery(GLuint id) const {
|
||||
Lock lock(_trashMutex);
|
||||
_queriesTrash.push_back(id);
|
||||
_currentFrameTrash.queriesTrash.push_back(id);
|
||||
}
|
||||
|
||||
void GLBackend::queueLambda(const std::function<void()> lambda) const {
|
||||
|
@ -746,6 +746,81 @@ void GLBackend::queueLambda(const std::function<void()> lambda) const {
|
|||
_lambdaQueue.push_back(lambda);
|
||||
}
|
||||
|
||||
void GLBackend::FrameTrash::cleanup() {
|
||||
glWaitSync(fence, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync(fence);
|
||||
|
||||
{
|
||||
std::vector<GLuint> ids;
|
||||
ids.reserve(buffersTrash.size());
|
||||
for (auto pair : buffersTrash) {
|
||||
ids.push_back(pair.first);
|
||||
}
|
||||
if (!ids.empty()) {
|
||||
glDeleteBuffers((GLsizei)ids.size(), ids.data());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<GLuint> ids;
|
||||
ids.reserve(framebuffersTrash.size());
|
||||
for (auto id : framebuffersTrash) {
|
||||
ids.push_back(id);
|
||||
}
|
||||
if (!ids.empty()) {
|
||||
glDeleteFramebuffers((GLsizei)ids.size(), ids.data());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<GLuint> ids;
|
||||
ids.reserve(texturesTrash.size());
|
||||
for (auto pair : texturesTrash) {
|
||||
ids.push_back(pair.first);
|
||||
}
|
||||
if (!ids.empty()) {
|
||||
glDeleteTextures((GLsizei)ids.size(), ids.data());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
if (!externalTexturesTrash.empty()) {
|
||||
std::vector<GLsync> fences;
|
||||
fences.resize(externalTexturesTrash.size());
|
||||
for (size_t i = 0; i < externalTexturesTrash.size(); ++i) {
|
||||
fences[i] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
}
|
||||
// External texture fences will be read in another thread/context, so we need a flush
|
||||
glFlush();
|
||||
size_t index = 0;
|
||||
for (auto pair : externalTexturesTrash) {
|
||||
auto fence = fences[index++];
|
||||
pair.second(pair.first, fence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (auto id : programsTrash) {
|
||||
glDeleteProgram(id);
|
||||
}
|
||||
|
||||
for (auto id : shadersTrash) {
|
||||
glDeleteShader(id);
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<GLuint> ids;
|
||||
ids.reserve(queriesTrash.size());
|
||||
for (auto id : queriesTrash) {
|
||||
ids.push_back(id);
|
||||
}
|
||||
if (!ids.empty()) {
|
||||
glDeleteQueries((GLsizei)ids.size(), ids.data());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GLBackend::recycle() const {
|
||||
PROFILE_RANGE(render_gpu_gl, __FUNCTION__)
|
||||
{
|
||||
|
@ -759,112 +834,16 @@ void GLBackend::recycle() const {
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<GLuint> ids;
|
||||
std::list<std::pair<GLuint, Size>> buffersTrash;
|
||||
{
|
||||
Lock lock(_trashMutex);
|
||||
std::swap(_buffersTrash, buffersTrash);
|
||||
}
|
||||
ids.reserve(buffersTrash.size());
|
||||
for (auto pair : buffersTrash) {
|
||||
ids.push_back(pair.first);
|
||||
}
|
||||
if (!ids.empty()) {
|
||||
glDeleteBuffers((GLsizei)ids.size(), ids.data());
|
||||
}
|
||||
while (!_previousFrameTrashes.empty()) {
|
||||
_previousFrameTrashes.front().cleanup();
|
||||
_previousFrameTrashes.pop_front();
|
||||
}
|
||||
|
||||
_previousFrameTrashes.emplace_back();
|
||||
{
|
||||
std::vector<GLuint> ids;
|
||||
std::list<GLuint> framebuffersTrash;
|
||||
{
|
||||
Lock lock(_trashMutex);
|
||||
std::swap(_framebuffersTrash, framebuffersTrash);
|
||||
}
|
||||
ids.reserve(framebuffersTrash.size());
|
||||
for (auto id : framebuffersTrash) {
|
||||
ids.push_back(id);
|
||||
}
|
||||
if (!ids.empty()) {
|
||||
glDeleteFramebuffers((GLsizei)ids.size(), ids.data());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<GLuint> ids;
|
||||
std::list<std::pair<GLuint, Size>> texturesTrash;
|
||||
{
|
||||
Lock lock(_trashMutex);
|
||||
std::swap(_texturesTrash, texturesTrash);
|
||||
}
|
||||
ids.reserve(texturesTrash.size());
|
||||
for (auto pair : texturesTrash) {
|
||||
ids.push_back(pair.first);
|
||||
}
|
||||
if (!ids.empty()) {
|
||||
glDeleteTextures((GLsizei)ids.size(), ids.data());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::list<std::pair<GLuint, Texture::ExternalRecycler>> externalTexturesTrash;
|
||||
{
|
||||
Lock lock(_trashMutex);
|
||||
std::swap(_externalTexturesTrash, externalTexturesTrash);
|
||||
}
|
||||
if (!externalTexturesTrash.empty()) {
|
||||
std::vector<GLsync> fences;
|
||||
fences.resize(externalTexturesTrash.size());
|
||||
for (size_t i = 0; i < externalTexturesTrash.size(); ++i) {
|
||||
fences[i] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
}
|
||||
// External texture fences will be read in another thread/context, so we need a flush
|
||||
glFlush();
|
||||
size_t index = 0;
|
||||
for (auto pair : externalTexturesTrash) {
|
||||
auto fence = fences[index++];
|
||||
pair.second(pair.first, fence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::list<GLuint> programsTrash;
|
||||
{
|
||||
Lock lock(_trashMutex);
|
||||
std::swap(_programsTrash, programsTrash);
|
||||
}
|
||||
for (auto id : programsTrash) {
|
||||
glDeleteProgram(id);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::list<GLuint> shadersTrash;
|
||||
{
|
||||
Lock lock(_trashMutex);
|
||||
std::swap(_shadersTrash, shadersTrash);
|
||||
}
|
||||
for (auto id : shadersTrash) {
|
||||
glDeleteShader(id);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<GLuint> ids;
|
||||
std::list<GLuint> queriesTrash;
|
||||
{
|
||||
Lock lock(_trashMutex);
|
||||
std::swap(_queriesTrash, queriesTrash);
|
||||
}
|
||||
ids.reserve(queriesTrash.size());
|
||||
for (auto id : queriesTrash) {
|
||||
ids.push_back(id);
|
||||
}
|
||||
if (!ids.empty()) {
|
||||
glDeleteQueries((GLsizei)ids.size(), ids.data());
|
||||
}
|
||||
Lock lock(_trashMutex);
|
||||
_previousFrameTrashes.back().swap(_currentFrameTrash);
|
||||
_previousFrameTrashes.back().fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
}
|
||||
|
||||
_textureManagement._transferEngine->manageMemory();
|
||||
|
|
|
@ -419,16 +419,34 @@ protected:
|
|||
static const size_t INVALID_OFFSET = (size_t)-1;
|
||||
bool _inRenderTransferPass{ false };
|
||||
int _currentDraw{ -1 };
|
||||
|
||||
std::list<std::string> profileRanges;
|
||||
|
||||
struct FrameTrash {
|
||||
GLsync fence = nullptr;
|
||||
std::list<std::pair<GLuint, Size>> buffersTrash;
|
||||
std::list<std::pair<GLuint, Size>> texturesTrash;
|
||||
std::list<std::pair<GLuint, Texture::ExternalRecycler>> externalTexturesTrash;
|
||||
std::list<GLuint> framebuffersTrash;
|
||||
std::list<GLuint> shadersTrash;
|
||||
std::list<GLuint> programsTrash;
|
||||
std::list<GLuint> queriesTrash;
|
||||
|
||||
void swap(FrameTrash& other) {
|
||||
buffersTrash.swap(other.buffersTrash);
|
||||
texturesTrash.swap(other.texturesTrash);
|
||||
externalTexturesTrash.swap(other.externalTexturesTrash);
|
||||
framebuffersTrash.swap(other.framebuffersTrash);
|
||||
shadersTrash.swap(other.shadersTrash);
|
||||
programsTrash.swap(other.programsTrash);
|
||||
queriesTrash.swap(other.queriesTrash);
|
||||
}
|
||||
|
||||
void cleanup();
|
||||
};
|
||||
|
||||
mutable Mutex _trashMutex;
|
||||
mutable std::list<std::pair<GLuint, Size>> _buffersTrash;
|
||||
mutable std::list<std::pair<GLuint, Size>> _texturesTrash;
|
||||
mutable std::list<std::pair<GLuint, Texture::ExternalRecycler>> _externalTexturesTrash;
|
||||
mutable std::list<GLuint> _framebuffersTrash;
|
||||
mutable std::list<GLuint> _shadersTrash;
|
||||
mutable std::list<GLuint> _programsTrash;
|
||||
mutable std::list<GLuint> _queriesTrash;
|
||||
mutable FrameTrash _currentFrameTrash;
|
||||
mutable std::list<FrameTrash> _previousFrameTrashes;
|
||||
std::list<std::string> profileRanges;
|
||||
mutable std::list<std::function<void()>> _lambdaQueue;
|
||||
|
||||
void renderPassTransfer(const Batch& batch);
|
||||
|
|
|
@ -139,9 +139,9 @@ void RenderEventHandler::onRender() {
|
|||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
// Fence will be used in another thread / context, so a flush is required
|
||||
glFlush();
|
||||
_shared->updateTextureAndFence({ texture, fence });
|
||||
// Fence will be used in another thread / context, so a flush is required
|
||||
_shared->_quickWindow->resetOpenGLState();
|
||||
}
|
||||
}
|
||||
|
@ -167,4 +167,5 @@ void RenderEventHandler::onQuit() {
|
|||
moveToThread(qApp->thread());
|
||||
QThread::currentThread()->quit();
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
|
|
@ -51,8 +51,10 @@ uint32_t TextureCache::acquireTexture(const QSize& size) {
|
|||
if (!textureSet.returnedTextures.empty()) {
|
||||
auto textureAndFence = textureSet.returnedTextures.front();
|
||||
textureSet.returnedTextures.pop_front();
|
||||
glWaitSync((GLsync)textureAndFence.second, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync((GLsync)textureAndFence.second);
|
||||
if (textureAndFence.second) {
|
||||
glWaitSync((GLsync)textureAndFence.second, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync((GLsync)textureAndFence.second);
|
||||
}
|
||||
return textureAndFence.first;
|
||||
}
|
||||
return createTexture(size);
|
||||
|
@ -101,9 +103,11 @@ void TextureCache::destroyTexture(uint32_t texture) {
|
|||
|
||||
void TextureCache::destroy(const Value& textureAndFence) {
|
||||
const auto& fence = textureAndFence.second;
|
||||
// FIXME prevents crash on shutdown, but we should migrate to a global functions object owned by the shared context.
|
||||
glWaitSync((GLsync)fence, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync((GLsync)fence);
|
||||
if (fence) {
|
||||
// FIXME prevents crash on shutdown, but we should migrate to a global functions object owned by the shared context.
|
||||
glWaitSync((GLsync)fence, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync((GLsync)fence);
|
||||
}
|
||||
destroyTexture(textureAndFence.first);
|
||||
}
|
||||
|
||||
|
|
77
tests-manual/qml/qml/MacQml.qml
Normal file
77
tests-manual/qml/qml/MacQml.qml
Normal file
|
@ -0,0 +1,77 @@
|
|||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the examples of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:BSD$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** BSD License Usage
|
||||
** Alternatively, you may use this file under the terms of the BSD license
|
||||
** as follows:
|
||||
**
|
||||
** "Redistribution and use in source and binary forms, with or without
|
||||
** modification, are permitted provided that the following conditions are
|
||||
** met:
|
||||
** * Redistributions of source code must retain the above copyright
|
||||
** notice, this list of conditions and the following disclaimer.
|
||||
** * Redistributions in binary form must reproduce the above copyright
|
||||
** notice, this list of conditions and the following disclaimer in
|
||||
** the documentation and/or other materials provided with the
|
||||
** distribution.
|
||||
** * Neither the name of The Qt Company Ltd nor the names of its
|
||||
** contributors may be used to endorse or promote products derived
|
||||
** from this software without specific prior written permission.
|
||||
**
|
||||
**
|
||||
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Dialogs 1.1
|
||||
import QtQuick.Controls 1.2
|
||||
import QtWebEngine 1.5
|
||||
|
||||
Item {
|
||||
width: 640
|
||||
height: 480
|
||||
|
||||
Rectangle {
|
||||
width: 5
|
||||
height: 5
|
||||
color: "red"
|
||||
ColorAnimation on color { loops: Animation.Infinite; from: "red"; to: "yellow"; duration: 1000 }
|
||||
}
|
||||
|
||||
|
||||
WebEngineView {
|
||||
id: root
|
||||
url: "https://google.com/"
|
||||
x: 6; y: 6;
|
||||
width: parent.width * 0.8
|
||||
height: parent.height * 0.8
|
||||
|
||||
}
|
||||
}
|
78
tests-manual/qml/src/MacQml.cpp
Normal file
78
tests-manual/qml/src/MacQml.cpp
Normal file
|
@ -0,0 +1,78 @@
|
|||
#include "MacQml.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QtQuick/QQuickItem>
|
||||
|
||||
#include <SharedUtil.h>
|
||||
|
||||
using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence;
|
||||
//
|
||||
//void MacQml::destroySurface(QmlInfo& qmlInfo) {
|
||||
// auto& surface = qmlInfo.surface;
|
||||
// auto& currentTexture = qmlInfo.texture;
|
||||
// if (currentTexture) {
|
||||
// auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
// glFlush();
|
||||
// _discardLamdba(currentTexture, readFence);
|
||||
// }
|
||||
// auto webView = surface->getRootItem();
|
||||
// if (webView) {
|
||||
// // stop loading
|
||||
// QMetaObject::invokeMethod(webView, "stop");
|
||||
// webView->setProperty(URL_PROPERTY, "about:blank");
|
||||
// }
|
||||
// surface->pause();
|
||||
// surface.reset();
|
||||
//}
|
||||
|
||||
void MacQml::update() {
|
||||
auto rootItem =_surface->getRootItem();
|
||||
float now = sinf(secTimestampNow());
|
||||
rootItem->setProperty("level", abs(now));
|
||||
rootItem->setProperty("muted", now > 0.0f);
|
||||
rootItem->setProperty("statsValue", rand());
|
||||
|
||||
// Fetch any new textures
|
||||
TextureAndFence newTextureAndFence;
|
||||
if (_surface->fetchTexture(newTextureAndFence)) {
|
||||
if (_texture != 0) {
|
||||
auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
_discardLamdba(_texture, readFence);
|
||||
}
|
||||
_texture = newTextureAndFence.first;
|
||||
_glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED);
|
||||
}
|
||||
}
|
||||
|
||||
void MacQml::init() {
|
||||
Parent::init();
|
||||
_glf.glGenFramebuffers(1, &_fbo);
|
||||
_surface.reset(new hifi::qml::OffscreenSurface());
|
||||
//QUrl url =getTestResource("qml/main.qml");
|
||||
QUrl url = getTestResource("qml/MacQml.qml");
|
||||
hifi::qml::QmlContextObjectCallback callback =[](QQmlContext* context, QQuickItem* item) {
|
||||
};
|
||||
_surface->load(url, callback);
|
||||
_surface->resize(_window->size());
|
||||
_surface->resume();
|
||||
|
||||
}
|
||||
|
||||
void MacQml::draw() {
|
||||
auto size = _window->geometry().size();
|
||||
if (_texture) {
|
||||
_glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo);
|
||||
_glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
|
||||
_glf.glBlitFramebuffer(
|
||||
// src coordinates
|
||||
0, 0, size.width(), size.height(),
|
||||
// dst coordinates
|
||||
0, 0, size.width(), size.height(),
|
||||
// blit mask and filter
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
_glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
_glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
}
|
||||
}
|
16
tests-manual/qml/src/MacQml.h
Normal file
16
tests-manual/qml/src/MacQml.h
Normal file
|
@ -0,0 +1,16 @@
|
|||
#include "TestCase.h"
|
||||
|
||||
#include <qml/OffscreenSurface.h>
|
||||
|
||||
class MacQml : public TestCase {
|
||||
using Parent = TestCase;
|
||||
public:
|
||||
GLuint _texture{ 0 };
|
||||
QmlPtr _surface;
|
||||
GLuint _fbo{ 0 };
|
||||
|
||||
MacQml(const QWindow* window) : Parent(window) {}
|
||||
void update() override;
|
||||
void init() override;
|
||||
void draw() override;
|
||||
};
|
131
tests-manual/qml/src/StressWeb.cpp
Normal file
131
tests-manual/qml/src/StressWeb.cpp
Normal file
|
@ -0,0 +1,131 @@
|
|||
#include "StressWeb.h"
|
||||
|
||||
#include <QtQuick/QQuickItem>
|
||||
|
||||
#include <SharedUtil.h>
|
||||
|
||||
using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence;
|
||||
|
||||
static const int DEFAULT_MAX_FPS = 10;
|
||||
static const QString CONTROL_URL{ "/qml/controls/WebEntityView.qml" };
|
||||
static const char* URL_PROPERTY{ "url" };
|
||||
|
||||
QString StressWeb::getSourceUrl(bool video) {
|
||||
static const std::vector<QString> SOURCE_URLS{
|
||||
"https://www.reddit.com/wiki/random",
|
||||
"https://en.wikipedia.org/wiki/Wikipedia:Random",
|
||||
"https://slashdot.org/",
|
||||
};
|
||||
|
||||
static const std::vector<QString> VIDEO_SOURCE_URLS{
|
||||
"https://www.youtube.com/watch?v=gDXwhHm4GhM",
|
||||
"https://www.youtube.com/watch?v=Ch_hoYPPeGc",
|
||||
};
|
||||
|
||||
const auto& sourceUrls = video ? VIDEO_SOURCE_URLS : SOURCE_URLS;
|
||||
auto index = rand() % sourceUrls.size();
|
||||
return sourceUrls[index];
|
||||
}
|
||||
|
||||
|
||||
|
||||
void StressWeb::buildSurface(QmlInfo& qmlInfo, bool video) {
|
||||
++_surfaceCount;
|
||||
auto lifetimeSecs = (uint32_t)(5.0f + (randFloat() * 10.0f));
|
||||
auto lifetimeUsecs = (USECS_PER_SECOND * lifetimeSecs);
|
||||
qmlInfo.lifetime = lifetimeUsecs + usecTimestampNow();
|
||||
qmlInfo.texture = 0;
|
||||
qmlInfo.surface.reset(new hifi::qml::OffscreenSurface());
|
||||
qmlInfo.surface->load(getTestResource(CONTROL_URL), [video](QQmlContext* context, QQuickItem* item) {
|
||||
item->setProperty(URL_PROPERTY, getSourceUrl(video));
|
||||
});
|
||||
qmlInfo.surface->setMaxFps(DEFAULT_MAX_FPS);
|
||||
qmlInfo.surface->resize(_qmlSize);
|
||||
qmlInfo.surface->resume();
|
||||
}
|
||||
|
||||
void StressWeb::destroySurface(QmlInfo& qmlInfo) {
|
||||
auto& surface = qmlInfo.surface;
|
||||
auto& currentTexture = qmlInfo.texture;
|
||||
if (currentTexture) {
|
||||
auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
_discardLamdba(currentTexture, readFence);
|
||||
}
|
||||
auto webView = surface->getRootItem();
|
||||
if (webView) {
|
||||
// stop loading
|
||||
QMetaObject::invokeMethod(webView, "stop");
|
||||
webView->setProperty(URL_PROPERTY, "about:blank");
|
||||
}
|
||||
surface->pause();
|
||||
surface.reset();
|
||||
}
|
||||
|
||||
void StressWeb::update() {
|
||||
auto now = usecTimestampNow();
|
||||
// Fetch any new textures
|
||||
for (size_t x = 0; x < DIVISIONS_X; ++x) {
|
||||
for (size_t y = 0; y < DIVISIONS_Y; ++y) {
|
||||
auto& qmlInfo = _surfaces[x][y];
|
||||
if (!qmlInfo.surface) {
|
||||
if (now < _createStopTime && randFloat() > 0.99f) {
|
||||
buildSurface(qmlInfo, x == 0 && y == 0);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (now > qmlInfo.lifetime) {
|
||||
destroySurface(qmlInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& surface = qmlInfo.surface;
|
||||
auto& currentTexture = qmlInfo.texture;
|
||||
|
||||
TextureAndFence newTextureAndFence;
|
||||
if (surface->fetchTexture(newTextureAndFence)) {
|
||||
if (currentTexture != 0) {
|
||||
auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
_discardLamdba(currentTexture, readFence);
|
||||
}
|
||||
currentTexture = newTextureAndFence.first;
|
||||
_glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StressWeb::init() {
|
||||
Parent::init();
|
||||
_createStopTime = usecTimestampNow() + (3000u * USECS_PER_SECOND);
|
||||
_glf.glGenFramebuffers(1, &_fbo);
|
||||
}
|
||||
|
||||
void StressWeb::draw() {
|
||||
auto size = _window->geometry().size();
|
||||
auto incrementX = size.width() / DIVISIONS_X;
|
||||
auto incrementY = size.height() / DIVISIONS_Y;
|
||||
|
||||
for (uint32_t x = 0; x < DIVISIONS_X; ++x) {
|
||||
for (uint32_t y = 0; y < DIVISIONS_Y; ++y) {
|
||||
auto& qmlInfo = _surfaces[x][y];
|
||||
if (!qmlInfo.surface || !qmlInfo.texture) {
|
||||
continue;
|
||||
}
|
||||
_glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo);
|
||||
_glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, qmlInfo.texture, 0);
|
||||
_glf.glBlitFramebuffer(
|
||||
// src coordinates
|
||||
0, 0, _qmlSize.width() - 1, _qmlSize.height() - 1,
|
||||
// dst coordinates
|
||||
incrementX * x, incrementY * y, incrementX * (x + 1), incrementY * (y + 1),
|
||||
// blit mask and filter
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
}
|
||||
}
|
||||
_glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
_glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
}
|
34
tests-manual/qml/src/StressWeb.h
Normal file
34
tests-manual/qml/src/StressWeb.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
#include "TestCase.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <qml/OffscreenSurface.h>
|
||||
|
||||
#define DIVISIONS_X 5
|
||||
#define DIVISIONS_Y 5
|
||||
|
||||
class StressWeb : public TestCase {
|
||||
using Parent = TestCase;
|
||||
public:
|
||||
using QmlPtr = QSharedPointer<hifi::qml::OffscreenSurface>;
|
||||
|
||||
struct QmlInfo {
|
||||
QmlPtr surface;
|
||||
GLuint texture{ 0 };
|
||||
uint64_t lifetime{ 0 };
|
||||
};
|
||||
|
||||
size_t _surfaceCount{ 0 };
|
||||
uint64_t _createStopTime{ 0 };
|
||||
const QSize _qmlSize{ 640, 480 };
|
||||
std::array<std::array<QmlInfo, DIVISIONS_Y>, DIVISIONS_X> _surfaces;
|
||||
GLuint _fbo{ 0 };
|
||||
|
||||
StressWeb(const QWindow* window) : Parent(window) {}
|
||||
static QString getSourceUrl(bool video);
|
||||
void buildSurface(QmlInfo& qmlInfo, bool video);
|
||||
void destroySurface(QmlInfo& qmlInfo);
|
||||
void update() override;
|
||||
void init() override;
|
||||
void draw() override;
|
||||
};
|
25
tests-manual/qml/src/TestCase.cpp
Normal file
25
tests-manual/qml/src/TestCase.cpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#include "TestCase.h"
|
||||
|
||||
#include <QtCore/QLoggingCategory>
|
||||
#include <QtCore/QDir>
|
||||
|
||||
void TestCase::destroy() {
|
||||
}
|
||||
void TestCase::update() {
|
||||
}
|
||||
|
||||
void TestCase::init() {
|
||||
_glf.initializeOpenGLFunctions();
|
||||
_discardLamdba = hifi::qml::OffscreenSurface::getDiscardLambda();
|
||||
}
|
||||
|
||||
QUrl TestCase::getTestResource(const QString& relativePath) {
|
||||
static QString dir;
|
||||
if (dir.isEmpty()) {
|
||||
QDir path(__FILE__);
|
||||
path.cdUp();
|
||||
dir = path.cleanPath(path.absoluteFilePath("../")) + "/";
|
||||
qDebug() << "Resources Path: " << dir;
|
||||
}
|
||||
return QUrl::fromLocalFile(dir + relativePath);
|
||||
}
|
23
tests-manual/qml/src/TestCase.h
Normal file
23
tests-manual/qml/src/TestCase.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <QtGui/QWindow>
|
||||
#include <QtGui/QOpenGLFunctions_4_1_Core>
|
||||
#include <qml/OffscreenSurface.h>
|
||||
|
||||
class TestCase {
|
||||
public:
|
||||
using QmlPtr = QSharedPointer<hifi::qml::OffscreenSurface>;
|
||||
using Builder = std::function<TestCase*(const QWindow*)>;
|
||||
TestCase(const QWindow* window) : _window(window) {}
|
||||
virtual void init();
|
||||
virtual void destroy();
|
||||
virtual void update();
|
||||
virtual void draw() = 0;
|
||||
static QUrl getTestResource(const QString& relativePath);
|
||||
|
||||
protected:
|
||||
QOpenGLFunctions_4_1_Core _glf;
|
||||
const QWindow* _window;
|
||||
std::function<void(uint32_t, void*)> _discardLamdba;
|
||||
};
|
|
@ -43,6 +43,11 @@
|
|||
#include <qml/OffscreenSurface.h>
|
||||
#include <unordered_set>
|
||||
#include <array>
|
||||
#include <gl/GLHelpers.h>
|
||||
#include <gl/Context.h>
|
||||
|
||||
#include "TestCase.h"
|
||||
#include "MacQml.h"
|
||||
|
||||
namespace gl {
|
||||
extern void initModuleGl();
|
||||
|
@ -67,53 +72,37 @@ QUrl getTestResource(const QString& relativePath) {
|
|||
return QUrl::fromLocalFile(dir + relativePath);
|
||||
}
|
||||
|
||||
#define DIVISIONS_X 5
|
||||
#define DIVISIONS_Y 5
|
||||
|
||||
using QmlPtr = QSharedPointer<hifi::qml::OffscreenSurface>;
|
||||
using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence;
|
||||
|
||||
struct QmlInfo {
|
||||
QmlPtr surface;
|
||||
GLuint texture{ 0 };
|
||||
uint64_t lifetime{ 0 };
|
||||
};
|
||||
|
||||
class TestWindow : public QWindow {
|
||||
public:
|
||||
TestWindow();
|
||||
TestWindow(const TestCase::Builder& caseBuilder);
|
||||
|
||||
private:
|
||||
QOpenGLContext _glContext;
|
||||
OffscreenGLCanvas _sharedContext;
|
||||
std::array<std::array<QmlInfo, DIVISIONS_Y>, DIVISIONS_X> _surfaces;
|
||||
|
||||
TestCase* _testCase{ nullptr };
|
||||
QOpenGLFunctions_4_1_Core _glf;
|
||||
std::function<void(uint32_t, void*)> _discardLamdba;
|
||||
QSize _size;
|
||||
size_t _surfaceCount{ 0 };
|
||||
GLuint _fbo{ 0 };
|
||||
const QSize _qmlSize{ 640, 480 };
|
||||
bool _aboutToQuit{ false };
|
||||
uint64_t _createStopTime;
|
||||
void initGl();
|
||||
void updateSurfaces();
|
||||
void buildSurface(QmlInfo& qmlInfo, bool allowVideo);
|
||||
void destroySurface(QmlInfo& qmlInfo);
|
||||
void resizeWindow(const QSize& size);
|
||||
void draw();
|
||||
void resizeEvent(QResizeEvent* ev) override;
|
||||
};
|
||||
|
||||
TestWindow::TestWindow() {
|
||||
TestWindow::TestWindow(const TestCase::Builder& builder) {
|
||||
Setting::init();
|
||||
|
||||
_testCase = builder(this);
|
||||
|
||||
setSurfaceType(QSurface::OpenGLSurface);
|
||||
|
||||
qmlRegisterType<QTestItem>("Hifi", 1, 0, "TestItem");
|
||||
|
||||
show();
|
||||
_createStopTime = usecTimestampNow() + (3000u * USECS_PER_SECOND);
|
||||
|
||||
resize(QSize(800, 600));
|
||||
|
||||
|
@ -129,162 +118,84 @@ TestWindow::TestWindow() {
|
|||
});
|
||||
}
|
||||
|
||||
Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context);
|
||||
Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context();
|
||||
OffscreenGLCanvas* _chromiumShareContext{ nullptr};
|
||||
void TestWindow::initGl() {
|
||||
_glContext.setFormat(format());
|
||||
|
||||
auto globalShareContext = qt_gl_global_share_context();
|
||||
if (globalShareContext) {
|
||||
_glContext.setShareContext(globalShareContext);
|
||||
globalShareContext->makeCurrent(this);
|
||||
gl::Context::setupDebugLogging(globalShareContext);
|
||||
globalShareContext->doneCurrent();
|
||||
}
|
||||
|
||||
if (!_glContext.create() || !_glContext.makeCurrent(this)) {
|
||||
qFatal("Unable to intialize Window GL context");
|
||||
}
|
||||
gl::Context::setupDebugLogging(&_glContext);
|
||||
gl::initModuleGl();
|
||||
|
||||
_glf.initializeOpenGLFunctions();
|
||||
_glf.glGenFramebuffers(1, &_fbo);
|
||||
|
||||
if (!_sharedContext.create(&_glContext) || !_sharedContext.makeCurrent()) {
|
||||
qFatal("Unable to intialize Shared GL context");
|
||||
}
|
||||
hifi::qml::OffscreenSurface::setSharedContext(_sharedContext.getContext());
|
||||
_discardLamdba = hifi::qml::OffscreenSurface::getDiscardLambda();
|
||||
|
||||
if (!globalShareContext) {
|
||||
_chromiumShareContext = new OffscreenGLCanvas();
|
||||
_chromiumShareContext->setObjectName("ChromiumShareContext");
|
||||
_chromiumShareContext->create(&_glContext);
|
||||
if (!_chromiumShareContext->makeCurrent()) {
|
||||
qFatal("Unable to make chromium shared context current");
|
||||
}
|
||||
|
||||
qt_gl_set_global_share_context(_chromiumShareContext->getContext());
|
||||
_chromiumShareContext->doneCurrent();
|
||||
}
|
||||
|
||||
// Restore the GL widget context
|
||||
if (!_glContext.makeCurrent(this)) {
|
||||
qFatal("Unable to make window context current");
|
||||
}
|
||||
|
||||
_testCase->init();
|
||||
}
|
||||
|
||||
void TestWindow::resizeWindow(const QSize& size) {
|
||||
_size = size;
|
||||
}
|
||||
|
||||
static const int DEFAULT_MAX_FPS = 10;
|
||||
static const QString CONTROL_URL{ "/qml/controls/WebEntityView.qml" };
|
||||
static const char* URL_PROPERTY{ "url" };
|
||||
|
||||
QString getSourceUrl(bool video) {
|
||||
static const std::vector<QString> SOURCE_URLS{
|
||||
"https://www.reddit.com/wiki/random",
|
||||
"https://en.wikipedia.org/wiki/Wikipedia:Random",
|
||||
"https://slashdot.org/",
|
||||
};
|
||||
|
||||
static const std::vector<QString> VIDEO_SOURCE_URLS{
|
||||
"https://www.youtube.com/watch?v=gDXwhHm4GhM",
|
||||
"https://www.youtube.com/watch?v=Ch_hoYPPeGc",
|
||||
};
|
||||
|
||||
const auto& sourceUrls = video ? VIDEO_SOURCE_URLS : SOURCE_URLS;
|
||||
auto index = rand() % sourceUrls.size();
|
||||
return sourceUrls[index];
|
||||
}
|
||||
|
||||
void TestWindow::buildSurface(QmlInfo& qmlInfo, bool video) {
|
||||
++_surfaceCount;
|
||||
auto lifetimeSecs = (uint32_t)(5.0f + (randFloat() * 10.0f));
|
||||
auto lifetimeUsecs = (USECS_PER_SECOND * lifetimeSecs);
|
||||
qmlInfo.lifetime = lifetimeUsecs + usecTimestampNow();
|
||||
qmlInfo.texture = 0;
|
||||
qmlInfo.surface.reset(new hifi::qml::OffscreenSurface());
|
||||
qmlInfo.surface->load(getTestResource(CONTROL_URL), [video](QQmlContext* context, QQuickItem* item) {
|
||||
item->setProperty(URL_PROPERTY, getSourceUrl(video));
|
||||
});
|
||||
qmlInfo.surface->setMaxFps(DEFAULT_MAX_FPS);
|
||||
qmlInfo.surface->resize(_qmlSize);
|
||||
qmlInfo.surface->resume();
|
||||
}
|
||||
|
||||
void TestWindow::destroySurface(QmlInfo& qmlInfo) {
|
||||
auto& surface = qmlInfo.surface;
|
||||
auto& currentTexture = qmlInfo.texture;
|
||||
if (currentTexture) {
|
||||
auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
_discardLamdba(currentTexture, readFence);
|
||||
}
|
||||
auto webView = surface->getRootItem();
|
||||
if (webView) {
|
||||
// stop loading
|
||||
QMetaObject::invokeMethod(webView, "stop");
|
||||
webView->setProperty(URL_PROPERTY, "about:blank");
|
||||
}
|
||||
surface->pause();
|
||||
surface.reset();
|
||||
}
|
||||
|
||||
void TestWindow::updateSurfaces() {
|
||||
auto now = usecTimestampNow();
|
||||
// Fetch any new textures
|
||||
for (size_t x = 0; x < DIVISIONS_X; ++x) {
|
||||
for (size_t y = 0; y < DIVISIONS_Y; ++y) {
|
||||
auto& qmlInfo = _surfaces[x][y];
|
||||
if (!qmlInfo.surface) {
|
||||
if (now < _createStopTime && randFloat() > 0.99f) {
|
||||
buildSurface(qmlInfo, x == 0 && y == 0);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (now > qmlInfo.lifetime) {
|
||||
destroySurface(qmlInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& surface = qmlInfo.surface;
|
||||
auto& currentTexture = qmlInfo.texture;
|
||||
|
||||
TextureAndFence newTextureAndFence;
|
||||
if (surface->fetchTexture(newTextureAndFence)) {
|
||||
if (currentTexture != 0) {
|
||||
auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
glFlush();
|
||||
_discardLamdba(currentTexture, readFence);
|
||||
}
|
||||
currentTexture = newTextureAndFence.first;
|
||||
_glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestWindow::draw() {
|
||||
if (_aboutToQuit) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Attempting to draw before we're visible and have a valid size will
|
||||
// produce GL errors.
|
||||
if (!isVisible() || _size.width() <= 0 || _size.height() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [&] { initGl(); });
|
||||
|
||||
|
||||
if (!_glContext.makeCurrent(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateSurfaces();
|
||||
|
||||
auto size = this->geometry().size();
|
||||
auto incrementX = size.width() / DIVISIONS_X;
|
||||
auto incrementY = size.height() / DIVISIONS_Y;
|
||||
|
||||
_testCase->update();
|
||||
|
||||
auto size = geometry().size();
|
||||
_glf.glViewport(0, 0, size.width(), size.height());
|
||||
_glf.glClearColor(1, 0, 0, 1);
|
||||
_glf.glClear(GL_COLOR_BUFFER_BIT);
|
||||
for (uint32_t x = 0; x < DIVISIONS_X; ++x) {
|
||||
for (uint32_t y = 0; y < DIVISIONS_Y; ++y) {
|
||||
auto& qmlInfo = _surfaces[x][y];
|
||||
if (!qmlInfo.surface || !qmlInfo.texture) {
|
||||
continue;
|
||||
}
|
||||
_glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo);
|
||||
_glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, qmlInfo.texture, 0);
|
||||
_glf.glBlitFramebuffer(
|
||||
// src coordinates
|
||||
0, 0, _qmlSize.width() - 1, _qmlSize.height() - 1,
|
||||
// dst coordinates
|
||||
incrementX * x, incrementY * y, incrementX * (x + 1), incrementY * (y + 1),
|
||||
// blit mask and filter
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
}
|
||||
}
|
||||
_glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
_glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||
|
||||
_testCase->draw();
|
||||
|
||||
_glContext.swapBuffers(this);
|
||||
}
|
||||
|
||||
|
@ -292,19 +203,15 @@ void TestWindow::resizeEvent(QResizeEvent* ev) {
|
|||
resizeWindow(ev->size());
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
QSurfaceFormat format;
|
||||
format.setDepthBufferSize(24);
|
||||
format.setStencilBufferSize(8);
|
||||
int main(int argc, char** argv) {
|
||||
auto format = getDefaultOpenGLSurfaceFormat();
|
||||
format.setVersion(4, 1);
|
||||
format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
|
||||
format.setOption(QSurfaceFormat::DebugContext);
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
// setFormat(format);
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
TestWindow window;
|
||||
TestCase::Builder builder = [](const QWindow* window)->TestCase*{ return new MacQml(window); };
|
||||
TestWindow window(builder);
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue