/**************************************************************************** ** ** Copyright (C) 2019 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$ ** ****************************************************************************/ // Simple QRhiProfiler receiver app. Start it and then in a QRhi-based // application connect with a QTcpSocket to 127.0.0.1:30667 and set that as the // QRhiProfiler's device. #include #include #include #include #include #include #include #include #include #include const int MIN_KNOWN_OP = 1; const int MAX_KNOWN_OP = 18; class Parser : public QObject { Q_OBJECT public: void feed(const QByteArray &line); struct Event { QRhiProfiler::StreamOp op; qint64 timestamp; quint64 resource; QByteArray resourceName; struct Param { enum ValueType { Int64, Float }; QByteArray key; ValueType valueType; union { qint64 intValue; float floatValue; }; }; QVector params; const Param *param(const char *key) const { auto it = std::find_if(params.cbegin(), params.cend(), [key](const Param &p) { return !strcmp(p.key.constData(), key); }); return it == params.cend() ? nullptr : &*it; } }; signals: void eventReceived(const Event &e); }; void Parser::feed(const QByteArray &line) { const QList elems = line.split(','); if (elems.count() < 4) { qWarning("Malformed line '%s'", line.constData()); return; } bool ok = false; const int op = elems[0].toInt(&ok); if (!ok) { qWarning("Invalid op %s", elems[0].constData()); return; } if (op < MIN_KNOWN_OP || op > MAX_KNOWN_OP) { qWarning("Unknown op %d", op); return; } Event e; e.op = QRhiProfiler::StreamOp(op); e.timestamp = elems[1].toLongLong(); e.resource = elems[2].toULongLong(); e.resourceName = elems[3]; const int elemCount = elems.count(); for (int i = 4; i < elemCount; i += 2) { if (i + 1 < elemCount && !elems[i].isEmpty() && !elems[i + 1].isEmpty()) { QByteArray key = elems[i]; if (key.startsWith('F')) { key = key.mid(1); bool ok = false; const float value = elems[i + 1].toFloat(&ok); if (!ok) { qWarning("Failed to parse float %s in line '%s'", elems[i + 1].constData(), line.constData()); continue; } Event::Param param; param.key = key; param.valueType = Event::Param::Float; param.floatValue = value; e.params.append(param); } else { const qint64 value = elems[i + 1].toLongLong(); Event::Param param; param.key = key; param.valueType = Event::Param::Int64; param.intValue = value; e.params.append(param); } } } emit eventReceived(e); } class Tracker : public QObject { Q_OBJECT public slots: void handleEvent(const Parser::Event &e); signals: void buffersTouched(); void texturesTouched(); void swapchainsTouched(); void frameTimeTouched(); void gpuFrameTimeTouched(); void gpuMemAllocStatsTouched(); public: Tracker() { reset(); } static const int MAX_STAGING_SLOTS = 3; struct Buffer { Buffer() { memset(stagingExtraSize, 0, sizeof(stagingExtraSize)); } quint64 lastTimestamp; QByteArray resourceName; qint64 effectiveSize = 0; int backingGpuBufCount = 1; qint64 stagingExtraSize[MAX_STAGING_SLOTS]; }; QHash m_buffers; qint64 m_totalBufferApproxByteSize; qint64 m_peakBufferApproxByteSize; qint64 m_totalStagingBufferApproxByteSize; qint64 m_peakStagingBufferApproxByteSize; struct Texture { Texture() { memset(stagingExtraSize, 0, sizeof(stagingExtraSize)); } quint64 lastTimestamp; QByteArray resourceName; qint64 approxByteSize = 0; bool ownsNativeResource = true; qint64 stagingExtraSize[MAX_STAGING_SLOTS]; }; QHash m_textures; qint64 m_totalTextureApproxByteSize; qint64 m_peakTextureApproxByteSize; qint64 m_totalTextureStagingBufferApproxByteSize; qint64 m_peakTextureStagingBufferApproxByteSize; struct SwapChain { quint64 lastTimestamp; QByteArray resourceName; qint64 approxByteSize = 0; }; QHash m_swapchains; qint64 m_totalSwapChainApproxByteSize; qint64 m_peakSwapChainApproxByteSize; struct FrameTime { qint64 framesSinceResize = 0; int minDelta = 0; int maxDelta = 0; float avgDelta = 0; }; FrameTime m_lastFrameTime; struct GpuFrameTime { float minTime = 0; float maxTime = 0; float avgTime = 0; }; GpuFrameTime m_lastGpuFrameTime; struct GpuMemAllocStats { qint64 realAllocCount; qint64 subAllocCount; qint64 totalSize; qint64 unusedSize; }; GpuMemAllocStats m_lastGpuMemAllocStats; void reset() { m_buffers.clear(); m_textures.clear(); m_totalBufferApproxByteSize = 0; m_peakBufferApproxByteSize = 0; m_totalStagingBufferApproxByteSize = 0; m_peakStagingBufferApproxByteSize = 0; m_totalTextureApproxByteSize = 0; m_peakTextureApproxByteSize = 0; m_totalTextureStagingBufferApproxByteSize = 0; m_peakTextureStagingBufferApproxByteSize = 0; m_totalSwapChainApproxByteSize = 0; m_peakSwapChainApproxByteSize = 0; m_lastFrameTime = FrameTime(); m_lastGpuFrameTime = GpuFrameTime(); m_lastGpuMemAllocStats = GpuMemAllocStats(); emit buffersTouched(); emit texturesTouched(); emit swapchainsTouched(); emit frameTimeTouched(); emit gpuFrameTimeTouched(); emit gpuMemAllocStatsTouched(); } }; Q_DECLARE_TYPEINFO(Tracker::Buffer, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Tracker::Texture, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Tracker::FrameTime, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Tracker::GpuFrameTime, Q_MOVABLE_TYPE); void Tracker::handleEvent(const Parser::Event &e) { switch (e.op) { case QRhiProfiler::NewBuffer: { Buffer b; b.lastTimestamp = e.timestamp; b.resourceName = e.resourceName; // type,0,usage,1,logical_size,84,effective_size,84,backing_gpu_buf_count,1,backing_cpu_buf_count,0 for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("effective_size")) b.effectiveSize = p.intValue; else if (p.key == QByteArrayLiteral("backing_gpu_buf_count")) b.backingGpuBufCount = p.intValue; } m_totalBufferApproxByteSize += b.effectiveSize * b.backingGpuBufCount; m_peakBufferApproxByteSize = qMax(m_peakBufferApproxByteSize, m_totalBufferApproxByteSize); m_buffers.insert(e.resource, b); emit buffersTouched(); } break; case QRhiProfiler::ReleaseBuffer: { auto it = m_buffers.find(e.resource); if (it != m_buffers.end()) { m_totalBufferApproxByteSize -= it->effectiveSize * it->backingGpuBufCount; m_buffers.erase(it); emit buffersTouched(); } } break; case QRhiProfiler::NewBufferStagingArea: { qint64 slot = -1; qint64 size = 0; for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("slot")) slot = p.intValue; else if (p.key == QByteArrayLiteral("size")) size = p.intValue; } if (slot >= 0 && slot < MAX_STAGING_SLOTS) { auto it = m_buffers.find(e.resource); if (it != m_buffers.end()) { it->stagingExtraSize[slot] = size; m_totalStagingBufferApproxByteSize += size; m_peakStagingBufferApproxByteSize = qMax(m_peakStagingBufferApproxByteSize, m_totalStagingBufferApproxByteSize); emit buffersTouched(); } } } break; case QRhiProfiler::ReleaseBufferStagingArea: { qint64 slot = -1; for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("slot")) slot = p.intValue; } if (slot >= 0 && slot < MAX_STAGING_SLOTS) { auto it = m_buffers.find(e.resource); if (it != m_buffers.end()) { m_totalStagingBufferApproxByteSize -= it->stagingExtraSize[slot]; it->stagingExtraSize[slot] = 0; emit buffersTouched(); } } } break; case QRhiProfiler::NewTexture: { Texture t; t.lastTimestamp = e.timestamp; t.resourceName = e.resourceName; // width,256,height,256,format,1,owns_native_resource,1,mip_count,9,layer_count,1,effective_sample_count,1,approx_byte_size,349524 for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("approx_byte_size")) t.approxByteSize = p.intValue; else if (p.key == QByteArrayLiteral("owns_native_resource")) t.ownsNativeResource = p.intValue; } if (t.ownsNativeResource) { m_totalTextureApproxByteSize += t.approxByteSize; m_peakTextureApproxByteSize = qMax(m_peakTextureApproxByteSize, m_totalTextureApproxByteSize); } m_textures.insert(e.resource, t); emit texturesTouched(); } break; case QRhiProfiler::ReleaseTexture: { auto it = m_textures.find(e.resource); if (it != m_textures.end()) { if (it->ownsNativeResource) m_totalTextureApproxByteSize -= it->approxByteSize; m_textures.erase(it); emit texturesTouched(); } } break; case QRhiProfiler::NewTextureStagingArea: { qint64 slot = -1; qint64 size = 0; for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("slot")) slot = p.intValue; else if (p.key == QByteArrayLiteral("size")) size = p.intValue; } if (slot >= 0 && slot < MAX_STAGING_SLOTS) { auto it = m_textures.find(e.resource); if (it != m_textures.end()) { it->stagingExtraSize[slot] = size; m_totalTextureStagingBufferApproxByteSize += size; m_peakTextureStagingBufferApproxByteSize = qMax(m_peakTextureStagingBufferApproxByteSize, m_totalTextureStagingBufferApproxByteSize); emit texturesTouched(); } } } break; case QRhiProfiler::ReleaseTextureStagingArea: { qint64 slot = -1; for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("slot")) slot = p.intValue; } if (slot >= 0 && slot < MAX_STAGING_SLOTS) { auto it = m_textures.find(e.resource); if (it != m_textures.end()) { m_totalTextureStagingBufferApproxByteSize -= it->stagingExtraSize[slot]; it->stagingExtraSize[slot] = 0; emit texturesTouched(); } } } break; case QRhiProfiler::ResizeSwapChain: { auto it = m_swapchains.find(e.resource); if (it != m_swapchains.end()) m_totalSwapChainApproxByteSize -= it->approxByteSize; SwapChain s; s.lastTimestamp = e.timestamp; s.resourceName = e.resourceName; // width,1280,height,720,buffer_count,2,msaa_buffer_count,0,effective_sample_count,1,approx_total_byte_size,7372800 for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("approx_total_byte_size")) s.approxByteSize = p.intValue; } m_totalSwapChainApproxByteSize += s.approxByteSize; m_peakSwapChainApproxByteSize = qMax(m_peakSwapChainApproxByteSize, m_totalSwapChainApproxByteSize); m_swapchains.insert(e.resource, s); emit swapchainsTouched(); } break; case QRhiProfiler::ReleaseSwapChain: { auto it = m_swapchains.find(e.resource); if (it != m_swapchains.end()) { m_totalSwapChainApproxByteSize -= it->approxByteSize; m_swapchains.erase(it); emit swapchainsTouched(); } } break; case QRhiProfiler::GpuFrameTime: { // Fmin_ms_gpu_frame_time,0.15488,Fmax_ms_gpu_frame_time,0.494592,Favg_ms_gpu_frame_time,0.33462 for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("min_ms_gpu_frame_time")) m_lastGpuFrameTime.minTime = p.floatValue; else if (p.key == QByteArrayLiteral("max_ms_gpu_frame_time")) m_lastGpuFrameTime.maxTime = p.floatValue; else if (p.key == QByteArrayLiteral("avg_ms_gpu_frame_time")) m_lastGpuFrameTime.avgTime = p.floatValue; } emit gpuFrameTimeTouched(); } break; case QRhiProfiler::FrameToFrameTime: { // frames_since_resize,121,min_ms_frame_delta,9,max_ms_frame_delta,33,Favg_ms_frame_delta,16.1167 for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("frames_since_resize")) m_lastFrameTime.framesSinceResize = p.intValue; else if (p.key == QByteArrayLiteral("min_ms_frame_delta")) m_lastFrameTime.minDelta = p.intValue; else if (p.key == QByteArrayLiteral("max_ms_frame_delta")) m_lastFrameTime.maxDelta = p.intValue; else if (p.key == QByteArrayLiteral("avg_ms_frame_delta")) m_lastFrameTime.avgDelta = p.floatValue; } emit frameTimeTouched(); } break; case QRhiProfiler::GpuMemAllocStats: { // real_alloc_count,2,sub_alloc_count,154,total_size,10752,unused_size,50320896 for (const Parser::Event::Param &p : e.params) { if (p.key == QByteArrayLiteral("real_alloc_count")) m_lastGpuMemAllocStats.realAllocCount = p.intValue; else if (p.key == QByteArrayLiteral("sub_alloc_count")) m_lastGpuMemAllocStats.subAllocCount = p.intValue; else if (p.key == QByteArrayLiteral("total_size")) m_lastGpuMemAllocStats.totalSize = p.intValue; else if (p.key == QByteArrayLiteral("unused_size")) m_lastGpuMemAllocStats.unusedSize = p.intValue; } emit gpuMemAllocStatsTouched(); } break; default: break; } } class Server : public QTcpServer { Q_OBJECT protected: void incomingConnection(qintptr socketDescriptor) override; signals: void clientConnected(); void clientDisconnected(); void receiveStarted(); void lineReceived(const QByteArray &line); private: bool m_valid = false; QTcpSocket m_socket; QByteArray m_buf; }; void Server::incomingConnection(qintptr socketDescriptor) { if (m_valid) return; m_socket.setSocketDescriptor(socketDescriptor); m_valid = true; emit clientConnected(); connect(&m_socket, &QAbstractSocket::readyRead, this, [this] { bool receiveStartedSent = false; m_buf += m_socket.readAll(); while (m_buf.contains('\n')) { const int lfpos = m_buf.indexOf('\n'); const QByteArray line = m_buf.left(lfpos).trimmed(); m_buf = m_buf.mid(lfpos + 1); if (!receiveStartedSent) { receiveStartedSent = true; emit receiveStarted(); } emit lineReceived(line); } }); connect(&m_socket, &QAbstractSocket::disconnected, this, [this] { if (m_valid) { m_valid = false; emit clientDisconnected(); } }); } int main(int argc, char **argv) { QApplication app(argc, argv); Tracker tracker; Parser parser; QObject::connect(&parser, &Parser::eventReceived, &tracker, &Tracker::handleEvent); Server server; if (!server.listen(QHostAddress::Any, 30667)) qFatal("Failed to start server: %s", qPrintable(server.errorString())); QVBoxLayout *layout = new QVBoxLayout; QLabel *infoLabel = new QLabel(QLatin1String("Launch a Qt Quick application with QSG_RHI_PROFILE=1 and QSG_RHI_PROFILE_HOST set to the IP address.
" "(resource memory usage reporting works best with the Vulkan backend)
")); layout->addWidget(infoLabel); QGroupBox *groupBox = new QGroupBox(QLatin1String("RHI statistics")); QVBoxLayout *groupLayout = new QVBoxLayout; QLabel *buffersLabel = new QLabel; QObject::connect(&tracker, &Tracker::buffersTouched, buffersLabel, [buffersLabel, &tracker] { const QString msg = QString::asprintf("%d buffers with ca. %lld bytes of current memory (sub)allocations (peak %lld) + %lld bytes of known staging buffers (peak %lld)", tracker.m_buffers.count(), tracker.m_totalBufferApproxByteSize, tracker.m_peakBufferApproxByteSize, tracker.m_totalStagingBufferApproxByteSize, tracker.m_peakStagingBufferApproxByteSize); buffersLabel->setText(msg); }); groupLayout->addWidget(buffersLabel); QLabel *texturesLabel = new QLabel; QObject::connect(&tracker, &Tracker::texturesTouched, texturesLabel, [texturesLabel, &tracker] { const QString msg = QString::asprintf("%d textures with ca. %lld bytes of current memory (sub)allocations (peak %lld) + %lld bytes of known staging buffers (peak %lld)", tracker.m_textures.count(), tracker.m_totalTextureApproxByteSize, tracker.m_peakTextureApproxByteSize, tracker.m_totalTextureStagingBufferApproxByteSize, tracker.m_peakTextureStagingBufferApproxByteSize); texturesLabel->setText(msg); }); groupLayout->addWidget(texturesLabel); QLabel *swapchainsLabel = new QLabel; QObject::connect(&tracker, &Tracker::swapchainsTouched, swapchainsLabel, [swapchainsLabel, &tracker] { const QString msg = QString::asprintf("Estimated total swapchain color buffer size is %lld bytes (peak %lld)", tracker.m_totalSwapChainApproxByteSize, tracker.m_peakSwapChainApproxByteSize); swapchainsLabel->setText(msg); }); groupLayout->addWidget(swapchainsLabel); QLabel *frameTimeLabel = new QLabel; QObject::connect(&tracker, &Tracker::frameTimeTouched, frameTimeLabel, [frameTimeLabel, &tracker] { const QString msg = QString::asprintf("Frames since resize %lld Frame delta min %d ms max %d ms avg %f ms", tracker.m_lastFrameTime.framesSinceResize, tracker.m_lastFrameTime.minDelta, tracker.m_lastFrameTime.maxDelta, tracker.m_lastFrameTime.avgDelta); frameTimeLabel->setText(msg); }); groupLayout->addWidget(frameTimeLabel); QLabel *gpuFrameTimeLabel = new QLabel; QObject::connect(&tracker, &Tracker::gpuFrameTimeTouched, gpuFrameTimeLabel, [gpuFrameTimeLabel, &tracker] { const QString msg = QString::asprintf("GPU frame time min %f ms max %f ms avg %f ms", tracker.m_lastGpuFrameTime.minTime, tracker.m_lastGpuFrameTime.maxTime, tracker.m_lastGpuFrameTime.avgTime); gpuFrameTimeLabel->setText(msg); }); groupLayout->addWidget(gpuFrameTimeLabel); QLabel *gpuMemAllocStatsLabel = new QLabel; QObject::connect(&tracker, &Tracker::gpuMemAllocStatsTouched, gpuMemAllocStatsLabel, [gpuMemAllocStatsLabel, &tracker] { const QString msg = QString::asprintf("GPU memory allocator status: %lld real allocations %lld sub-allocations %lld total bytes %lld unused bytes", tracker.m_lastGpuMemAllocStats.realAllocCount, tracker.m_lastGpuMemAllocStats.subAllocCount, tracker.m_lastGpuMemAllocStats.totalSize, tracker.m_lastGpuMemAllocStats.unusedSize); gpuMemAllocStatsLabel->setText(msg); }); groupLayout->addWidget(gpuMemAllocStatsLabel); groupBox->setLayout(groupLayout); layout->addWidget(groupBox); QTextEdit *rawLog = new QTextEdit; rawLog->setReadOnly(true); layout->addWidget(rawLog); QObject::connect(&server, &Server::clientConnected, rawLog, [rawLog] { rawLog->append(QLatin1String("\nCONNECTED\n")); }); QObject::connect(&server, &Server::clientDisconnected, rawLog, [rawLog, &tracker] { rawLog->append(QLatin1String("\nDISCONNECTED\n")); tracker.reset(); }); QObject::connect(&server, &Server::receiveStarted, rawLog, [rawLog] { rawLog->setFontItalic(true); rawLog->append(QLatin1String("[") + QTime::currentTime().toString() + QLatin1String("]")); rawLog->setFontItalic(false); }); QObject::connect(&server, &Server::lineReceived, rawLog, [rawLog, &parser](const QByteArray &line) { rawLog->append(QString::fromUtf8(line)); parser.feed(line); }); QWidget w; w.resize(800, 600); w.setLayout(layout); w.show(); return app.exec(); } #include "qrhiprof.moc"