fgviewer: visualize intermediate buffers (#9862)
* Intermediate Buffer Visualization: Enabled live monitoring of internal
`FrameGraph` render targets directly in the `fgviewer` web UI.
* HTTP Polling Architecture: Switched from WebSocket binary pushes to native
`<img>` polling (`/api/image`)
* Robust Resource Tracking: Replaced string-based lookups with
`(ViewId, ResourceId)` composite keys to prevent cross-view collisions and
ensure accurate reads.
* Format Post-Processing: Extracted readback conversions (HDR tonemapping,
depth normalization, MSAA downsampling, single-channel expansion) into
`DebugServer`.
* UI Polish: Added a live-updating full-screen image modal and explicitly
filtered internal debug passes from the Graphviz/JSON exports to prevent
DOM thrashing.
* WIP: Currently Unsupported:
* GL backend: failed with:
OpenGL framebuffer error 0x8cd6 (GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT) in "readTexture" at line 4198
* Mipmaps & Subresources: Reading specific mip levels or array layers is
explicitly skipped.
* Shadowmaps: Variance Shadow Maps (VSM) will physically evaluate to `0.0`
and appear completely empty/black in scenes without active shadow
casters (due to inverted-Z).
* Stencil: Resolving and reading stencil buffer data is not supported by
the backend.
* WIP: Untested outside of MacOS+metal/vk
This commit is contained in:
@@ -156,6 +156,7 @@ set(SRCS
|
||||
src/ds/StructureDescriptorSet.cpp
|
||||
src/fg/Blackboard.cpp
|
||||
src/fg/DependencyGraph.cpp
|
||||
src/fg/FgviewerManager.cpp
|
||||
src/fg/FrameGraph.cpp
|
||||
src/fg/FrameGraphPass.cpp
|
||||
src/fg/FrameGraphResources.cpp
|
||||
|
||||
@@ -284,6 +284,7 @@ public:
|
||||
switch (srcType) {
|
||||
case UBYTE: reshaper = reshapeImageImpl<float, uint8_t>; break;
|
||||
case FLOAT: reshaper = reshapeImageImpl<float, float>; break;
|
||||
case HALF: reshaper = reshapeImageImpl<float, math::half>; break;
|
||||
case INT: reshaper = reshapeImageImpl<float, int32_t>; break;
|
||||
case UINT: reshaper = reshapeImageImpl<float, uint32_t>; break;
|
||||
case UINT_10F_11F_11F_REV:
|
||||
@@ -300,6 +301,7 @@ public:
|
||||
switch (srcType) {
|
||||
case UBYTE: reshaper = reshapeImageImpl<int32_t, uint8_t>; break;
|
||||
case FLOAT: reshaper = reshapeImageImpl<int32_t, float>; break;
|
||||
case HALF: reshaper = reshapeImageImpl<int32_t, math::half>; break;
|
||||
case INT: reshaper = reshapeImageImpl<int32_t, int32_t>; break;
|
||||
case UINT: reshaper = reshapeImageImpl<int32_t, uint32_t>; break;
|
||||
case UINT_10F_11F_11F_REV:
|
||||
@@ -316,6 +318,7 @@ public:
|
||||
switch (srcType) {
|
||||
case UBYTE: reshaper = reshapeImageImpl<uint32_t, uint8_t>; break;
|
||||
case FLOAT: reshaper = reshapeImageImpl<uint32_t, float>; break;
|
||||
case HALF: reshaper = reshapeImageImpl<uint32_t, math::half>; break;
|
||||
case INT: reshaper = reshapeImageImpl<uint32_t, int32_t>; break;
|
||||
case UINT: reshaper = reshapeImageImpl<uint32_t, uint32_t>; break;
|
||||
case UINT_10F_11F_11F_REV:
|
||||
|
||||
@@ -3687,7 +3687,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::blitDepth(FrameGraph& fg,
|
||||
|
||||
FrameGraphId<FrameGraphTexture> PostProcessManager::resolve(FrameGraph& fg,
|
||||
utils::StaticString outputBufferName, FrameGraphId<FrameGraphTexture> const input,
|
||||
FrameGraphTexture::Descriptor outDesc) noexcept {
|
||||
FrameGraphTexture::Descriptor outDesc, utils::CString customPassName) noexcept {
|
||||
|
||||
// Don't do anything if we're not a MSAA buffer
|
||||
auto const& inDesc = fg.getDescriptor(input);
|
||||
@@ -3701,7 +3701,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolve(FrameGraph& fg,
|
||||
// through shaders or some other manipulation.
|
||||
if ((isDepthFormat(inDesc.format) || isStencilFormat(inDesc.format)) &&
|
||||
(!mDepthStencilResolveSupported)) {
|
||||
return resolveDepthWithShader(fg, outputBufferName, input, outDesc);
|
||||
return resolveDepthWithShader(fg, outputBufferName, input, outDesc,
|
||||
std::move(customPassName));
|
||||
}
|
||||
|
||||
outDesc.width = inDesc.width;
|
||||
@@ -3714,7 +3715,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolve(FrameGraph& fg,
|
||||
FrameGraphId<FrameGraphTexture> output;
|
||||
};
|
||||
|
||||
auto const& ppResolve = fg.addPass<ResolveData>("resolve",
|
||||
auto const& ppResolve = fg.addPass<ResolveData>(
|
||||
customPassName.empty() ? "resolve" : customPassName.c_str(),
|
||||
[&](FrameGraph::Builder& builder, auto& data) {
|
||||
data.input = builder.read(input, FrameGraphTexture::Usage::BLIT_SRC);
|
||||
data.output = builder.createTexture(outputBufferName, outDesc);
|
||||
@@ -3742,7 +3744,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolve(FrameGraph& fg,
|
||||
|
||||
FrameGraphId<FrameGraphTexture> PostProcessManager::resolveDepthWithShader(FrameGraph& fg,
|
||||
utils::StaticString outputBufferName, FrameGraphId<FrameGraphTexture> const input,
|
||||
FrameGraphTexture::Descriptor outDesc) noexcept {
|
||||
FrameGraphTexture::Descriptor outDesc, utils::CString customPassName) noexcept {
|
||||
|
||||
// Don't do anything if we're not a MSAA buffer
|
||||
auto const& inDesc = fg.getDescriptor(input);
|
||||
@@ -3765,7 +3767,8 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::resolveDepthWithShader(Frame
|
||||
FrameGraphId<FrameGraphTexture> output;
|
||||
};
|
||||
|
||||
auto const& ppResolve = fg.addPass<ResolveData>("resolveDepthWithShader",
|
||||
auto const& ppResolve = fg.addPass<ResolveData>(
|
||||
customPassName.empty() ? "resolveDepthWithShader" : customPassName.c_str(),
|
||||
[&](FrameGraph::Builder& builder, auto& data) {
|
||||
data.input = builder.sample(input);
|
||||
data.output = builder.createTexture(outputBufferName, outDesc);
|
||||
|
||||
@@ -302,16 +302,19 @@ public:
|
||||
|
||||
// Resolves base level of input and outputs a texture from outDesc.
|
||||
// outDesc with, height, format and samples will be overridden.
|
||||
FrameGraphId<FrameGraphTexture> resolve(FrameGraph& fg,
|
||||
utils::StaticString outputBufferName, FrameGraphId<FrameGraphTexture> input,
|
||||
FrameGraphTexture::Descriptor outDesc) noexcept;
|
||||
// customPassName is an optional parameter to override the default "resolve" pass name.
|
||||
FrameGraphId<FrameGraphTexture> resolve(FrameGraph& fg, utils::StaticString outputBufferName,
|
||||
FrameGraphId<FrameGraphTexture> input, FrameGraphTexture::Descriptor outDesc,
|
||||
utils::CString customPassName = {}) noexcept;
|
||||
|
||||
// Resolves base level of input and outputs a texture from outDesc using a shader instead of
|
||||
// driver-implemented API.
|
||||
// outDesc with, height, format and samples will be overridden.
|
||||
// customPassName is an optional parameter to override the default "resolveDepthWithShader" pass
|
||||
// name.
|
||||
FrameGraphId<FrameGraphTexture> resolveDepthWithShader(FrameGraph& fg,
|
||||
utils::StaticString outputBufferName, FrameGraphId<FrameGraphTexture> input,
|
||||
FrameGraphTexture::Descriptor outDesc) noexcept;
|
||||
FrameGraphTexture::Descriptor outDesc, utils::CString customPassName = {}) noexcept;
|
||||
|
||||
FrameGraphId<FrameGraphTexture> gaussianBlurPass(FrameGraph& fg,
|
||||
FrameGraphId<FrameGraphTexture> input,
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
#include "fg/FgviewerManager.h"
|
||||
#endif
|
||||
|
||||
#include "generated/resources/materials.h"
|
||||
|
||||
using namespace filament::math;
|
||||
@@ -108,6 +112,27 @@ using namespace filaflat;
|
||||
|
||||
namespace {
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER || FILAMENT_ENABLE_MATDBG
|
||||
utils::CString getPortString(std::string_view serviceType) {
|
||||
#ifndef __ANDROID__
|
||||
char const* portString = getenv(serviceType.data());
|
||||
#else
|
||||
char const* portString = [&]() -> char const*{
|
||||
if (serviceType == "FILAMENT_MATDBG_PORT") {
|
||||
return "8081";
|
||||
} else if (serviceType == "FILAMENT_FGVIEWER_PORT") {
|
||||
return "8085";
|
||||
}
|
||||
return nullptr;
|
||||
}();
|
||||
#endif
|
||||
if (portString) {
|
||||
return utils::CString { portString };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
#endif // FILAMENT_ENABLE_FGVIEWER || FILAMENT_ENABLE_MATDBG
|
||||
|
||||
Platform::DriverConfig getDriverConfig(FEngine* instance) {
|
||||
Platform::DriverConfig const driverConfig {
|
||||
.featureFlagManager = instance,
|
||||
@@ -870,13 +895,8 @@ int FEngine::loop() {
|
||||
}
|
||||
|
||||
#if FILAMENT_ENABLE_MATDBG
|
||||
#ifdef __ANDROID__
|
||||
const char* portString = "8081";
|
||||
#else
|
||||
const char* portString = getenv("FILAMENT_MATDBG_PORT");
|
||||
#endif
|
||||
if (portString != nullptr) {
|
||||
const int port = atoi(portString);
|
||||
if (auto portString = getPortString("FILAMENT_MATDBG_PORT"); !portString.empty()) {
|
||||
const int port = atoi(portString.c_str());
|
||||
|
||||
ShaderLanguage preferredLanguage = ShaderLanguage::UNSPECIFIED;
|
||||
if (mBackend == Backend::METAL) {
|
||||
@@ -900,20 +920,13 @@ int FEngine::loop() {
|
||||
#endif
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER // NOLINT(*-include-cleaner)
|
||||
#ifdef __ANDROID__
|
||||
const char* fgviewerPortString = "8085";
|
||||
#else
|
||||
const char* fgviewerPortString = getenv("FILAMENT_FGVIEWER_PORT");
|
||||
#endif
|
||||
if (fgviewerPortString != nullptr) {
|
||||
const int fgviewerPort = atoi(fgviewerPortString);
|
||||
debug.fgviewerServer = new fgviewer::DebugServer(fgviewerPort);
|
||||
|
||||
if (auto portString = getPortString("FILAMENT_FGVIEWER_PORT"); !portString.empty()) {
|
||||
debug.fgviewer = new FgviewerManager(*this, std::move(portString));
|
||||
// Sometimes the server can fail to spin up (e.g. if the above port is already in use).
|
||||
// When this occurs, carry onward, developers can look at civetweb.txt for details.
|
||||
if (!debug.fgviewerServer->isReady()) {
|
||||
delete debug.fgviewerServer;
|
||||
debug.fgviewerServer = nullptr;
|
||||
if (!debug.fgviewer->isReady()) {
|
||||
delete debug.fgviewer;
|
||||
debug.fgviewer = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -930,8 +943,8 @@ int FEngine::loop() {
|
||||
}
|
||||
#endif
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
if(debug.fgviewerServer) {
|
||||
delete debug.fgviewerServer;
|
||||
if (debug.fgviewer) {
|
||||
delete debug.fgviewer;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1723,7 +1736,7 @@ FixedCapacityVector<Variant> FEngine::getMaterialCompileVariants(
|
||||
if (Variant::isValidDepthVariant(depthVariant)) {
|
||||
// if we have a valid depth variant, add the stereo and skinning bits
|
||||
depthVariant.setStereo(view->hasStereo());
|
||||
|
||||
|
||||
size_t const depthStart = variants.size();
|
||||
variants.push_back(depthVariant);
|
||||
apply(depthStart, skinning, &Variant::setSkinning);
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
#include "details/Skybox.h"
|
||||
#include "details/Sync.h"
|
||||
|
||||
|
||||
#include <private/filament/EngineEnums.h>
|
||||
|
||||
#include <private/backend/CommandBufferQueue.h>
|
||||
@@ -99,16 +98,8 @@ using MaterialKey = uint32_t;
|
||||
} // namespace filament::matdbg
|
||||
#endif
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
#include <fgviewer/DebugServer.h>
|
||||
#else
|
||||
namespace filament::fgviewer {
|
||||
class DebugServer;
|
||||
} // namespace filament::fgviewer
|
||||
#endif
|
||||
|
||||
namespace filament {
|
||||
|
||||
class FgviewerManager;
|
||||
class Renderer;
|
||||
class MaterialParser;
|
||||
class TextureCacheDisposer;
|
||||
@@ -771,7 +762,7 @@ public:
|
||||
bool combine_multiview_images = false;
|
||||
} stereo;
|
||||
matdbg::DebugServer* server = nullptr;
|
||||
fgviewer::DebugServer* fgviewerServer = nullptr;
|
||||
FgviewerManager* fgviewer = nullptr;
|
||||
} debug;
|
||||
};
|
||||
|
||||
|
||||
@@ -884,6 +884,13 @@ void FRenderer::renderJob(DriverApi& driver, RootArenaScope& rootArenaScope, FVi
|
||||
*/
|
||||
FrameGraph fg(*mResourceAllocator,
|
||||
isProtectedContent ? FrameGraph::Mode::PROTECTED : FrameGraph::Mode::UNPROTECTED);
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
if (UTILS_LIKELY(engine.debug.fgviewer)) {
|
||||
fg.setFgviewerData(engine.debug.fgviewer, &view);
|
||||
}
|
||||
#endif
|
||||
|
||||
auto& blackboard = fg.getBlackboard();
|
||||
|
||||
PostProcessManager::ScreenSpaceRefConfig const ssrConfig = PostProcessManager::prepareMipmapSSR(
|
||||
@@ -1550,18 +1557,10 @@ void FRenderer::renderJob(DriverApi& driver, RootArenaScope& rootArenaScope, FVi
|
||||
// fg.forwardResource(fgViewRenderTarget, debug ? debug : input);
|
||||
|
||||
fg.forwardResource(fgViewRenderTarget, input);
|
||||
|
||||
fg.present(fgViewRenderTarget);
|
||||
|
||||
fg.compile();
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
fgviewer::DebugServer* fgviewerServer = engine.debug.fgviewerServer;
|
||||
if (UTILS_LIKELY(fgviewerServer)) {
|
||||
fgviewerServer->update(view.getViewHandle(), fg.getFrameGraphInfo(view.getName()));
|
||||
}
|
||||
#endif
|
||||
|
||||
//utils::io::sstream graphviz;
|
||||
//fg.export_graphviz(graphviz, view.getName());
|
||||
//DLOG(INFO) << graphviz.c_str();
|
||||
|
||||
@@ -84,6 +84,10 @@
|
||||
|
||||
using namespace utils;
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
#include "fg/FgviewerManager.h"
|
||||
#endif
|
||||
|
||||
namespace filament {
|
||||
|
||||
using namespace backend;
|
||||
@@ -172,10 +176,10 @@ FView::FView(FEngine& engine)
|
||||
#endif
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
fgviewer::DebugServer* fgviewerServer = engine.debug.fgviewerServer;
|
||||
if (UTILS_LIKELY(fgviewerServer)) {
|
||||
FgviewerManager* fgviewerManager = engine.debug.fgviewer;
|
||||
if (UTILS_LIKELY(fgviewerManager)) {
|
||||
mFrameGraphViewerViewHandle =
|
||||
fgviewerServer->createView(utils::CString(getName()));
|
||||
fgviewerManager->createView(utils::CString(getName()));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -225,9 +229,9 @@ void FView::terminate(FEngine& engine) {
|
||||
#endif
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
fgviewer::DebugServer* fgviewerServer = engine.debug.fgviewerServer;
|
||||
if (UTILS_LIKELY(fgviewerServer)) {
|
||||
fgviewerServer->destroyView(mFrameGraphViewerViewHandle);
|
||||
FgviewerManager* fgviewerManager = engine.debug.fgviewer;
|
||||
if (UTILS_LIKELY(fgviewerManager)) {
|
||||
fgviewerManager->destroyView(mFrameGraphViewerViewHandle);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
#include <utils/CString.h>
|
||||
#include <utils/ostream.h>
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
#include <fgviewer/FrameGraphInfo.h>
|
||||
#endif
|
||||
|
||||
#include <iterator>
|
||||
#include <cstdint>
|
||||
|
||||
@@ -155,14 +159,33 @@ void DependencyGraph::export_graphviz(utils::io::ostream& out, char const* name)
|
||||
|
||||
auto const& nodes = mNodes;
|
||||
|
||||
for (Node const* node : nodes) {
|
||||
auto skipNode = [](char const* name) {
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
std::string_view view{ name };
|
||||
return view == std::string_view(fgviewer::READBACK_PASS_NAME) ||
|
||||
view == std::string_view(fgviewer::RESOLVED_MONITOR_PASS_NAME);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
};
|
||||
|
||||
for (Node const* node: nodes) {
|
||||
if (skipNode(node->getName())) {
|
||||
continue;
|
||||
}
|
||||
uint32_t id = node->getId();
|
||||
utils::CString s = node->graphvizify();
|
||||
out << "\"N" << id << "\" " << s.c_str() << "\n";
|
||||
}
|
||||
|
||||
out << "\n";
|
||||
for (Node const* node : nodes) {
|
||||
for (Node const* node: nodes) {
|
||||
if (skipNode(node->getName())) {
|
||||
continue;
|
||||
}
|
||||
uint32_t id = node->getId();
|
||||
|
||||
auto edges = getOutgoingEdges(node);
|
||||
@@ -174,22 +197,48 @@ void DependencyGraph::export_graphviz(utils::io::ostream& out, char const* name)
|
||||
|
||||
// render the valid edges
|
||||
if (first != pos) {
|
||||
out << "N" << id << " -> { ";
|
||||
while (first != pos) {
|
||||
Node const* ref = getNode((*first++)->to);
|
||||
out << "N" << ref->getId() << " ";
|
||||
bool hasValidEdges = false;
|
||||
for (auto it = first; it != pos; ++it) {
|
||||
Node const* ref = getNode((*it)->to);
|
||||
if (skipNode(ref->getName())) {
|
||||
continue;
|
||||
}
|
||||
hasValidEdges = true;
|
||||
}
|
||||
if (hasValidEdges) {
|
||||
out << "N" << id << " -> { ";
|
||||
while (first != pos) {
|
||||
Node const* ref = getNode((*first++)->to);
|
||||
if (skipNode(ref->getName())) {
|
||||
continue;
|
||||
}
|
||||
out << "N" << ref->getId() << " ";
|
||||
}
|
||||
out << "} [color=" << s.c_str() << "2]\n";
|
||||
}
|
||||
out << "} [color=" << s.c_str() << "2]\n";
|
||||
}
|
||||
|
||||
// render the invalid edges
|
||||
if (first != edges.end()) {
|
||||
out << "N" << id << " -> { ";
|
||||
while (first != edges.end()) {
|
||||
Node const* ref = getNode((*first++)->to);
|
||||
out << "N" << ref->getId() << " ";
|
||||
bool hasInvalidEdges = false;
|
||||
for (auto it = first; it != edges.end(); ++it) {
|
||||
Node const* ref = getNode((*it)->to);
|
||||
if (skipNode(ref->getName())) {
|
||||
continue;
|
||||
}
|
||||
hasInvalidEdges = true;
|
||||
}
|
||||
if (hasInvalidEdges) {
|
||||
out << "N" << id << " -> { ";
|
||||
while (first != edges.end()) {
|
||||
Node const* ref = getNode((*first++)->to);
|
||||
if (skipNode(ref->getName())) {
|
||||
continue;
|
||||
}
|
||||
out << "N" << ref->getId() << " ";
|
||||
}
|
||||
out << "} [color=" << s.c_str() << "4 style=dashed]\n";
|
||||
}
|
||||
out << "} [color=" << s.c_str() << "4 style=dashed]\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
280
filament/src/fg/FgviewerManager.cpp
Normal file
280
filament/src/fg/FgviewerManager.cpp
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
|
||||
#include "fg/FgviewerManager.h"
|
||||
|
||||
#include "details/View.h"
|
||||
|
||||
#include "fg/FrameGraph.h"
|
||||
#include "fg/FrameGraphId.h"
|
||||
#include "fg/FrameGraphResources.h"
|
||||
#include "fg/FrameGraphTexture.h"
|
||||
|
||||
#include <fgviewer/DebugServer.h>
|
||||
#include <fgviewer/FrameGraphInfo.h>
|
||||
|
||||
#include <backend/DriverEnums.h>
|
||||
#include <backend/Handle.h>
|
||||
#include <backend/PixelBufferDescriptor.h>
|
||||
|
||||
#include <utils/CString.h>
|
||||
#include <utils/compiler.h>
|
||||
#include <utils/Log.h>
|
||||
|
||||
namespace filament {
|
||||
|
||||
FgviewerManager::FgviewerManager(FEngine& engine, utils::CString const& serverPort)
|
||||
: mServer(std::make_unique<fgviewer::DebugServer>(atoi(serverPort.c_str()),
|
||||
[this](fgviewer::ViewHandle viewId, uint32_t id, utils::CString const& name,
|
||||
Request::Callback&& callback) {
|
||||
requestTextureReadback(viewId, id, name, std::move(callback));
|
||||
})),
|
||||
mPostProcessManager(engine.getPostProcessManager()) {}
|
||||
|
||||
bool FgviewerManager::isReady() const { return mServer->isReady(); }
|
||||
|
||||
fgviewer::ViewHandle FgviewerManager::createView(utils::CString name) {
|
||||
return mServer->createView(std::move(name));
|
||||
}
|
||||
|
||||
void FgviewerManager::destroyView(fgviewer::ViewHandle h) {
|
||||
mServer->destroyView(h);
|
||||
}
|
||||
|
||||
void FgviewerManager::addReadbacksToFramegraph(FrameGraph& fg,
|
||||
backend::Handle<backend::HwRenderTarget> viewTarget, uint32_t viewId) {
|
||||
std::vector<Request> requests;
|
||||
|
||||
{
|
||||
std::unique_lock<utils::Mutex> lock(mReadbackRequestsMutex);
|
||||
std::swap(requests, mReadbackRequests);
|
||||
}
|
||||
|
||||
// Keep requests that belong to other views in the queue
|
||||
std::vector<Request> remainingRequests;
|
||||
|
||||
for (auto& request: requests) {
|
||||
auto emptyResponse = [&request]() {
|
||||
if (!request.callback) {
|
||||
return;
|
||||
}
|
||||
request.callback(std::vector<unsigned char>{}, 0, 0,
|
||||
fgviewer::DebugServer::PixelDataFormat::RGBA,
|
||||
fgviewer::DebugServer::FormatInfo{});
|
||||
};
|
||||
|
||||
if (request.viewId != viewId) {
|
||||
remainingRequests.push_back(std::move(request));
|
||||
continue;
|
||||
}
|
||||
|
||||
FrameGraphId<FrameGraphTexture> fgTexture =
|
||||
fg.getTextureByIdName(request.id, request.name.c_str());
|
||||
if (!fgTexture.isInitialized()) {
|
||||
utils::slog.e << "[fgviewer] Requested texture id " << request.id << " "
|
||||
<< request.name.c_str() << " not found in FrameGraph."
|
||||
<< "not found in FrameGraph." << utils::io::endl;
|
||||
|
||||
// Dump all resources for debugging
|
||||
utils::slog.e << "[fgviewer] Dumping all FrameGraph resources:" << utils::io::endl;
|
||||
fg.dumpResources();
|
||||
emptyResponse();
|
||||
continue;
|
||||
}
|
||||
|
||||
FrameGraphTexture::Descriptor const& texDescOriginal = fg.getDescriptor(fgTexture);
|
||||
utils::CString const requestName = request.name;
|
||||
std::string_view const requestNameView{ requestName.data(), requestName.length() };
|
||||
bool const isViewRenderTarget = requestNameView == "viewRenderTarget";
|
||||
|
||||
// TODO: mip or multi-layer readback is not supported for fgviewer.
|
||||
bool const isMip = requestNameView.find("mip") != std::string_view::npos ||
|
||||
requestNameView.find("Layer") != std::string_view::npos;
|
||||
|
||||
if (isMip) {
|
||||
utils::slog.w << "[fgviewer] WARNING: Readback for subresources (" << requestName.c_str()
|
||||
<< " ) is not supported." << utils::io::endl;
|
||||
emptyResponse();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (texDescOriginal.samples > 1 && !isViewRenderTarget) {
|
||||
PostProcessManager& ppm = mPostProcessManager;
|
||||
fgTexture = ppm.resolve(fg, "Resolved Monitor", fgTexture, { .levels = 1 },
|
||||
utils::CString(fgviewer::RESOLVED_MONITOR_PASS_NAME));
|
||||
}
|
||||
|
||||
FrameGraphTexture::Descriptor const& texDesc = fg.getDescriptor(fgTexture);
|
||||
bool const isDepth =
|
||||
backend::isDepthFormat(texDesc.format) || backend::isStencilFormat(texDesc.format);
|
||||
|
||||
struct ReadbackPassData {
|
||||
FrameGraphId<FrameGraphTexture> texture;
|
||||
};
|
||||
|
||||
fg.addPass<ReadbackPassData>(
|
||||
fgviewer::READBACK_PASS_NAME,
|
||||
[&](FrameGraph::Builder& builder, ReadbackPassData& data) {
|
||||
FrameGraphTexture::Usage usage =
|
||||
isViewRenderTarget ? FrameGraphTexture::Usage::COLOR_ATTACHMENT
|
||||
: FrameGraphTexture::Usage::SAMPLEABLE;
|
||||
data.texture = builder.read(fgTexture, usage);
|
||||
builder.sideEffect(); // Ensure this pass is not culled
|
||||
},
|
||||
[request = std::move(request), requestName, isDepth, isViewRenderTarget,
|
||||
viewTarget](FrameGraphResources const& resources,
|
||||
ReadbackPassData const& data, backend::DriverApi& d) {
|
||||
const FrameGraphTexture::Descriptor& desc =
|
||||
resources.getDescriptor(data.texture);
|
||||
|
||||
backend::TextureFormat texFormat = desc.format;
|
||||
bool const isD24 = (texFormat == backend::TextureFormat::DEPTH24 ||
|
||||
texFormat == backend::TextureFormat::DEPTH24_STENCIL8);
|
||||
bool const isD16 = (texFormat == backend::TextureFormat::DEPTH16);
|
||||
bool const isD32F = (texFormat == backend::TextureFormat::DEPTH32F ||
|
||||
texFormat == backend::TextureFormat::DEPTH32F_STENCIL8);
|
||||
|
||||
backend::PixelBufferDescriptor::PixelDataFormat format =
|
||||
isDepth ? backend::PixelDataFormat::DEPTH_COMPONENT
|
||||
: backend::PixelDataFormat::RGBA;
|
||||
|
||||
bool const isFloat = texFormat == backend::TextureFormat::RGB16F ||
|
||||
texFormat == backend::TextureFormat::RGBA16F ||
|
||||
texFormat == backend::TextureFormat::R11F_G11F_B10F ||
|
||||
texFormat == backend::TextureFormat::RGB9_E5;
|
||||
|
||||
bool const isR8 = texFormat == backend::TextureFormat::R8 ||
|
||||
texFormat == backend::TextureFormat::R8_SNORM ||
|
||||
texFormat == backend::TextureFormat::R8UI ||
|
||||
texFormat == backend::TextureFormat::R8I;
|
||||
|
||||
backend::PixelBufferDescriptor::PixelDataType type;
|
||||
if (isDepth) {
|
||||
if (isD32F) {
|
||||
type = backend::PixelDataType::FLOAT;
|
||||
} else if (isD16) {
|
||||
type = backend::PixelDataType::USHORT;
|
||||
} else {
|
||||
type = backend::PixelDataType::UINT;
|
||||
}
|
||||
} else {
|
||||
type = isFloat ? backend::PixelDataType::FLOAT
|
||||
: backend::PixelDataType::UBYTE;
|
||||
}
|
||||
|
||||
fgviewer::DebugServer::PixelDataFormat const targetFormat =
|
||||
fgviewer::DebugServer::Format::RGBA;
|
||||
|
||||
const size_t bytesPerPixel =
|
||||
isDepth ? (isD32F ? 4 : (isD16 ? 2 : 4))
|
||||
: (type == backend::PixelDataType::FLOAT ? 16 : 4);
|
||||
const size_t samples = desc.samples > 1 ? desc.samples : 1;
|
||||
const size_t bufferSize = desc.width * desc.height * bytesPerPixel * samples;
|
||||
fgviewer::DebugServer::PixelBuffer pixelBuffer(bufferSize);
|
||||
void* bufferData = pixelBuffer.data();
|
||||
fgviewer::DebugServer::FormatInfo info{
|
||||
.isDepth = isDepth,
|
||||
.isD16 = isD16,
|
||||
.isD24 = isD24,
|
||||
.isD32F = isD32F,
|
||||
.isFloat = isFloat,
|
||||
.isR8 = isR8,
|
||||
.samples = static_cast<uint8_t>(samples),
|
||||
};
|
||||
|
||||
struct UserData {
|
||||
size_t width;
|
||||
size_t height;
|
||||
fgviewer::DebugServer::PixelDataFormat targetFormat;
|
||||
Request::Callback callback;
|
||||
fgviewer::DebugServer::PixelBuffer pixelBuffer;
|
||||
fgviewer::DebugServer::FormatInfo info;
|
||||
};
|
||||
|
||||
backend::PixelBufferDescriptor pbd(
|
||||
bufferData, bufferSize, format, type, 1, 0, 0, 0, 0,
|
||||
[](void* buffer, size_t size, void* user) {
|
||||
std::unique_ptr<UserData> d{ static_cast<UserData*>(user) };
|
||||
auto& callback = d->callback;
|
||||
if (callback) {
|
||||
callback(std::move(d->pixelBuffer), d->width, d->height,
|
||||
d->targetFormat, d->info);
|
||||
}
|
||||
},
|
||||
new UserData{ desc.width, desc.height, targetFormat,
|
||||
std::move(request.callback), std::move(pixelBuffer), info });
|
||||
|
||||
if (isViewRenderTarget) {
|
||||
if (viewTarget) {
|
||||
d.readPixels(viewTarget, 0, 0, desc.width, desc.height, std::move(pbd));
|
||||
} else {
|
||||
utils::slog.e << "[fgviewer] ERROR: Hardware RenderTarget is "
|
||||
"uninitialized for "
|
||||
<< requestName.c_str() << utils::io::endl;
|
||||
if (request.callback) {
|
||||
request.callback(std::vector<unsigned char>{}, 0, 0,
|
||||
fgviewer::DebugServer::Format::RGBA,
|
||||
fgviewer::DebugServer::FormatInfo{});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto hwTex = resources.getTexture(data.texture);
|
||||
if (hwTex) {
|
||||
auto srd = resources.getSubResourceDescriptor(data.texture);
|
||||
d.readTexture(hwTex, srd.level, srd.layer, 0, 0, desc.width,
|
||||
desc.height, std::move(pbd));
|
||||
} else {
|
||||
utils::slog.e << "[fgviewer] ERROR: Hardware Texture is uninitialized for "
|
||||
<< requestName.c_str() << utils::io::endl;
|
||||
if (request.callback) {
|
||||
request.callback(std::vector<unsigned char>{}, 0, 0,
|
||||
fgviewer::DebugServer::Format::RGBA,
|
||||
fgviewer::DebugServer::FormatInfo{});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!remainingRequests.empty()) {
|
||||
std::unique_lock<utils::Mutex> lock(mReadbackRequestsMutex);
|
||||
mReadbackRequests.insert(mReadbackRequests.end(),
|
||||
std::make_move_iterator(remainingRequests.begin()),
|
||||
std::make_move_iterator(remainingRequests.end()));
|
||||
}
|
||||
}
|
||||
|
||||
void FgviewerManager::framegraphUpdated(FrameGraph& fg, FView const& view) {
|
||||
auto info = fg.getFrameGraphInfo(view.getName());
|
||||
mServer->update(view.getViewHandle(), std::move(info));
|
||||
}
|
||||
|
||||
void FgviewerManager::framegraphExecuted() {
|
||||
mServer->tick();
|
||||
}
|
||||
|
||||
void FgviewerManager::requestTextureReadback(fgviewer::ViewHandle viewId, uint32_t id,
|
||||
const utils::CString& name,
|
||||
Request::Callback&& callback) {
|
||||
std::unique_lock<utils::Mutex> lock(mReadbackRequestsMutex);
|
||||
mReadbackRequests.emplace_back(Request{ viewId, id, name, std::move(callback) });
|
||||
}
|
||||
|
||||
} // namespace filament
|
||||
|
||||
#endif // FILAMENT_ENABLE_FGVIEWER
|
||||
87
filament/src/fg/FgviewerManager.h
Normal file
87
filament/src/fg/FgviewerManager.h
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef TNT_FILAMENT_FGVIEWERMANAGER_H
|
||||
#define TNT_FILAMENT_FGVIEWERMANAGER_H
|
||||
|
||||
#include "details/Engine.h"
|
||||
#include "PostProcessManager.h"
|
||||
|
||||
#include <backend/Handle.h>
|
||||
|
||||
#include <fgviewer/DebugServer.h>
|
||||
|
||||
#include <utils/CString.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace filament {
|
||||
|
||||
class FEngine;
|
||||
class FView;
|
||||
class FrameGraph;
|
||||
|
||||
class FgviewerManager {
|
||||
|
||||
public:
|
||||
FgviewerManager(FEngine& engine, utils::CString const& serverPort);
|
||||
|
||||
// Encapsulates a texture readback request
|
||||
struct Request {
|
||||
using Callback = fgviewer::DebugServer::ReabackFinishedCallback;
|
||||
fgviewer::ViewHandle viewId;
|
||||
uint32_t id;
|
||||
utils::CString name;
|
||||
Callback callback;
|
||||
};
|
||||
|
||||
// FgViewer offsers the feature of viewing intermediate rendered textures (render targets).
|
||||
// Each texture client wants to view is a request to readback the corresponding texture in the
|
||||
// framegraph. This method adds readback passes to the framegraph for each readback request.
|
||||
void addReadbacksToFramegraph(FrameGraph& fg,
|
||||
backend::Handle<backend::HwRenderTarget> viewTarget, uint32_t viewId);
|
||||
|
||||
// Updates FgViewer's framegraph state with the latest.
|
||||
void framegraphUpdated(FrameGraph& fg, FView const& view);
|
||||
|
||||
// This signals to fgviewer that the framegraph has been executed.
|
||||
void framegraphExecuted();
|
||||
|
||||
// Returns whether the server has been initialized (e.g. it could have failed because another
|
||||
// process owns the port).
|
||||
bool isReady() const;
|
||||
|
||||
fgviewer::ViewHandle createView(utils::CString name);
|
||||
void destroyView(fgviewer::ViewHandle h);
|
||||
|
||||
private:
|
||||
void requestTextureReadback(fgviewer::ViewHandle viewId, uint32_t id,
|
||||
utils::CString const& name, Request::Callback&& callback);
|
||||
|
||||
std::unique_ptr<fgviewer::DebugServer> mServer;
|
||||
utils::Mutex mReadbackRequestsMutex;
|
||||
std::vector<Request> mReadbackRequests;
|
||||
|
||||
// Needed for adding a resolve pass for multi-sampled rendertargets.
|
||||
PostProcessManager& mPostProcessManager;
|
||||
};
|
||||
|
||||
} // namespace filament
|
||||
|
||||
#endif // TNT_FILAMENT_FGVIEWERMANAGER_H
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "FrameGraphRenderPass.h"
|
||||
#include "FrameGraphTexture.h"
|
||||
|
||||
#include "details/Engine.h"
|
||||
#include "details/View.h"
|
||||
|
||||
#include <backend/DriverEnums.h>
|
||||
#include <backend/Handle.h>
|
||||
@@ -77,7 +77,9 @@ FrameGraphId<FrameGraphTexture> FrameGraph::Builder::declareRenderPass(
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
FrameGraph::FrameGraph(TextureCacheInterface& resourceAllocator, Mode const mode)
|
||||
|
||||
FrameGraph::FrameGraph(TextureCacheInterface& resourceAllocator,
|
||||
Mode const mode)
|
||||
: mResourceAllocator(resourceAllocator),
|
||||
mArena("FrameGraph Arena", 262144),
|
||||
mMode(mode),
|
||||
@@ -92,6 +94,14 @@ FrameGraph::FrameGraph(TextureCacheInterface& resourceAllocator, Mode const mode
|
||||
mPassNodes.reserve(64);
|
||||
}
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
void FrameGraph::setFgviewerData(FgviewerManager* fgviewer, FView const* view) {
|
||||
mFgviewer = fgviewer;
|
||||
mView = view;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
UTILS_NOINLINE
|
||||
void FrameGraph::destroyInternal() noexcept {
|
||||
// the order of destruction is important here
|
||||
@@ -124,6 +134,12 @@ FrameGraph& FrameGraph::compile() noexcept {
|
||||
|
||||
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
// Add passes for reading back textures
|
||||
mFgviewer->addReadbacksToFramegraph(*this, mView->getRenderTargetHandle(),
|
||||
mView->getViewHandle());
|
||||
#endif
|
||||
|
||||
DependencyGraph& dependencyGraph = mGraph;
|
||||
|
||||
// first we cull unreachable nodes
|
||||
@@ -194,6 +210,9 @@ FrameGraph& FrameGraph::compile() noexcept {
|
||||
pNode->resolveResourceUsage(dependencyGraph);
|
||||
}
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
mFgviewer->framegraphUpdated(*this, *mView);
|
||||
#endif
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -235,6 +254,10 @@ void FrameGraph::execute(backend::DriverApi& driver) noexcept {
|
||||
driver.popGroupMarker();
|
||||
}
|
||||
driver.popGroupMarker();
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
mFgviewer->framegraphExecuted();
|
||||
#endif
|
||||
}
|
||||
|
||||
void FrameGraph::addPresentPass(const std::function<void(Builder&)>& setup) noexcept {
|
||||
@@ -484,8 +507,8 @@ void FrameGraph::export_graphviz(utils::io::ostream& out, char const* name) cons
|
||||
mGraph.export_graphviz(out, name);
|
||||
}
|
||||
|
||||
fgviewer::FrameGraphInfo FrameGraph::getFrameGraphInfo(const char *viewName) const {
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
fgviewer::FrameGraphInfo FrameGraph::getFrameGraphInfo(const char* viewName) const {
|
||||
fgviewer::FrameGraphInfo info{utils::CString(viewName)};
|
||||
std::vector<fgviewer::FrameGraphInfo::Pass> passes;
|
||||
|
||||
@@ -495,6 +518,12 @@ fgviewer::FrameGraphInfo FrameGraph::getFrameGraphInfo(const char *viewName) con
|
||||
PassNode *const pass = *first;
|
||||
++first;
|
||||
|
||||
auto const nameStrView = std::string_view(pass->getName());
|
||||
if (nameStrView == fgviewer::READBACK_PASS_NAME ||
|
||||
nameStrView == fgviewer::RESOLVED_MONITOR_PASS_NAME) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert_invariant(!pass->isCulled());
|
||||
std::vector<fgviewer::ResourceId> reads;
|
||||
auto const &readEdges = mGraph.getIncomingEdges(pass);
|
||||
@@ -528,7 +557,13 @@ fgviewer::FrameGraphInfo FrameGraph::getFrameGraphInfo(const char *viewName) con
|
||||
}
|
||||
|
||||
std::unordered_map<fgviewer::ResourceId, fgviewer::FrameGraphInfo::Resource> resources;
|
||||
for (const auto &resourceNode: mResourceNodes) {
|
||||
for (const auto& resourceNode: mResourceNodes) {
|
||||
auto const nameStrView = std::string_view(resourceNode->getName());
|
||||
if (nameStrView == fgviewer::READBACK_PASS_NAME ||
|
||||
nameStrView == fgviewer::RESOLVED_MONITOR_PASS_NAME) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const FrameGraphHandle resourceHandle = resourceNode->resourceHandle;
|
||||
if (resources.find(resourceHandle.index) != resources.end())
|
||||
continue;
|
||||
@@ -578,14 +613,62 @@ fgviewer::FrameGraphInfo FrameGraph::getFrameGraphInfo(const char *viewName) con
|
||||
info.setGraphvizData(utils::CString(out.c_str()));
|
||||
|
||||
return info;
|
||||
#else
|
||||
return fgviewer::FrameGraphInfo();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
|
||||
FrameGraphId<FrameGraphTexture> FrameGraph::getTextureByIdName(uint32_t id,
|
||||
const char* name) const {
|
||||
// 1. Try the fast path: check if the requested ID matches the expected name.
|
||||
if (id < mResourceSlots.size()) {
|
||||
auto const& slot = mResourceSlots[id];
|
||||
if (slot.rid > 0) {
|
||||
VirtualResource const* resource = mResources[slot.rid];
|
||||
if (resource && strcmp(resource->name.c_str(), name) == 0) {
|
||||
FrameGraphHandle handle;
|
||||
handle.index = (uint16_t) id;
|
||||
handle.version = slot.version;
|
||||
return FrameGraphId<FrameGraphTexture>(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Slow path fallback: graph shifts across frames, so the ID might be invalid.
|
||||
// Search all resource slots for a matching name.
|
||||
for (size_t i = 1; i < mResourceSlots.size(); ++i) {
|
||||
auto const& slot = mResourceSlots[i];
|
||||
if (slot.rid > 0) {
|
||||
VirtualResource const* resource = mResources[slot.rid];
|
||||
if (resource && strcmp(resource->name.c_str(), name) == 0) {
|
||||
FrameGraphHandle handle;
|
||||
handle.index = (uint16_t) i;
|
||||
handle.version = slot.version;
|
||||
return FrameGraphId<FrameGraphTexture>(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void FrameGraph::dumpResources() const {
|
||||
for (size_t i = 1; i < mResourceSlots.size(); ++i) {
|
||||
auto const& slot = mResourceSlots[i];
|
||||
if (slot.rid > 0) {
|
||||
VirtualResource const* resource = mResources[slot.rid];
|
||||
if (resource) {
|
||||
fprintf(stderr, "[fgviewer] Slot %zu rid %u: \"%s\"\n", i, (uint32_t) slot.rid,
|
||||
resource->name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
/*
|
||||
* Explicit template instantiation for FrameGraphTexture which is a known type,
|
||||
* to reduce compile time and code size.
|
||||
|
||||
@@ -38,10 +38,14 @@
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
#include <fgviewer/FrameGraphInfo.h>
|
||||
#include "fg/FgviewerManager.h"
|
||||
#else
|
||||
namespace filament::fgviewer {
|
||||
class FrameGraphInfo{};
|
||||
} // namespace filament::fgviewer
|
||||
namespace filament {
|
||||
class FgviewerManager;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace filament {
|
||||
@@ -234,6 +238,13 @@ public:
|
||||
|
||||
explicit FrameGraph(TextureCacheInterface& resourceAllocator,
|
||||
Mode mode = Mode::UNPROTECTED);
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
// When fgviewer is enabled, we update the fgviewer at different points of framegraph
|
||||
// life-cycles. The view is useful for associating a graph with the view it's rendering into.
|
||||
void setFgviewerData(FgviewerManager* manager, FView const* view);
|
||||
#endif
|
||||
|
||||
FrameGraph(FrameGraph const&) = delete;
|
||||
FrameGraph& operator=(FrameGraph const&) = delete;
|
||||
~FrameGraph() noexcept;
|
||||
@@ -448,8 +459,26 @@ public:
|
||||
* Export a fgviewer::FrameGraphInfo for current graph.
|
||||
* Note that this function should be called after FrameGraph::compile().
|
||||
*/
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
fgviewer::FrameGraphInfo getFrameGraphInfo(const char *viewName) const;
|
||||
|
||||
/**
|
||||
* Retrieve a texture handle by its name. This is used for debugging/visualization tools.
|
||||
* @param name Name of the resource
|
||||
* @return Handle to the texture, or an uninitialized handle if not found.
|
||||
*/
|
||||
FrameGraphId<FrameGraphTexture> getTextureByIdName(uint32_t id, const char* name) const;
|
||||
|
||||
/**
|
||||
* Dump all resources to log.
|
||||
*/
|
||||
void dumpResources() const;
|
||||
#else
|
||||
fgviewer::FrameGraphInfo getFrameGraphInfo(const char*) const {
|
||||
return fgviewer::FrameGraphInfo();
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
friend class FrameGraphResources;
|
||||
friend class PassNode;
|
||||
@@ -547,6 +576,11 @@ private:
|
||||
Vector<ResourceNode*> mResourceNodes;
|
||||
Vector<PassNode*> mPassNodes;
|
||||
Vector<PassNode*>::iterator mActivePassNodesEnd;
|
||||
|
||||
#if FILAMENT_ENABLE_FGVIEWER
|
||||
FgviewerManager* mFgviewer = nullptr;
|
||||
FView const* mView = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
template<typename Data, typename Setup, typename Execute>
|
||||
|
||||
@@ -24,7 +24,6 @@ set(SRCS
|
||||
src/DebugServer.cpp
|
||||
src/FrameGraphInfo.cpp
|
||||
src/JsonWriter.cpp
|
||||
src/WebSocketHandler.cpp
|
||||
)
|
||||
|
||||
# ==================================================================================================
|
||||
@@ -71,7 +70,7 @@ target_link_libraries(${TARGET} PUBLIC
|
||||
utils
|
||||
)
|
||||
|
||||
target_include_directories(${TARGET} PRIVATE ${filamat_SOURCE_DIR}/src)
|
||||
target_include_directories(${TARGET} PRIVATE ${filamat_SOURCE_DIR}/src ${FILAMENT}/third_party/stb)
|
||||
|
||||
target_include_directories(${TARGET} PUBLIC ${PUBLIC_HDR_DIR})
|
||||
set_target_properties(${TARGET} PROPERTIES FOLDER Libs)
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <utils/CString.h>
|
||||
#include <utils/Mutex.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -30,7 +32,6 @@ class CivetServer;
|
||||
namespace filament::fgviewer {
|
||||
|
||||
using ViewHandle = uint32_t;
|
||||
class WebSocketHandler;
|
||||
|
||||
/**
|
||||
* Server-side frame graph debugger.
|
||||
@@ -40,10 +41,36 @@ class WebSocketHandler;
|
||||
*/
|
||||
class DebugServer {
|
||||
public:
|
||||
enum class PixelDataFormat : uint8_t { R, RG, RGB, RGBA, L, LA };
|
||||
struct Format {
|
||||
static constexpr PixelDataFormat R = PixelDataFormat::R;
|
||||
static constexpr PixelDataFormat RG = PixelDataFormat::RG;
|
||||
static constexpr PixelDataFormat RGB = PixelDataFormat::RGB;
|
||||
static constexpr PixelDataFormat RGBA = PixelDataFormat::RGBA;
|
||||
static constexpr PixelDataFormat L = PixelDataFormat::L;
|
||||
static constexpr PixelDataFormat LA = PixelDataFormat::LA;
|
||||
};
|
||||
|
||||
struct FormatInfo {
|
||||
bool isDepth = false;
|
||||
bool isD16 = false;
|
||||
bool isD24 = false;
|
||||
bool isD32F = false;
|
||||
bool isFloat = false;
|
||||
bool isR8 = false;
|
||||
uint8_t samples = 1;
|
||||
};
|
||||
|
||||
using PixelBuffer = std::vector<uint8_t>;
|
||||
using ReabackFinishedCallback =
|
||||
std::function<void(PixelBuffer, uint32_t, uint32_t, PixelDataFormat, FormatInfo)>;
|
||||
using ReadbackRequest = std::function<void(ViewHandle, uint32_t, utils::CString const&,
|
||||
ReabackFinishedCallback)>;
|
||||
|
||||
static std::string_view const kSuccessHeader;
|
||||
static std::string_view const kErrorHeader;
|
||||
|
||||
explicit DebugServer(int port);
|
||||
explicit DebugServer(int port, ReadbackRequest request = {});
|
||||
~DebugServer();
|
||||
|
||||
/**
|
||||
@@ -59,7 +86,39 @@ public:
|
||||
/**
|
||||
* Updates the information for a given view.
|
||||
*/
|
||||
void update(ViewHandle h, FrameGraphInfo info);
|
||||
void update(ViewHandle h, FrameGraphInfo&& info);
|
||||
|
||||
/**
|
||||
* Ticks the server to trigger monitoring requests.
|
||||
*/
|
||||
void tick();
|
||||
|
||||
/**
|
||||
* Set a resource to be monitored.
|
||||
*/
|
||||
void setResourceMonitor(ViewHandle viewId, uint32_t resourceId,
|
||||
utils::CString const& resourceName, bool enabled);
|
||||
|
||||
/**
|
||||
* Clear all resource monitors.
|
||||
*/
|
||||
void clearResourceMonitors();
|
||||
|
||||
struct ActiveMonitor {
|
||||
ViewHandle viewId;
|
||||
uint32_t resourceId;
|
||||
utils::CString name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get currently active resource monitors.
|
||||
*/
|
||||
std::vector<ActiveMonitor> getMonitoredResources() const;
|
||||
|
||||
/**
|
||||
* Get the latest rendered image for a monitored resource.
|
||||
*/
|
||||
std::vector<uint8_t> getImage(ViewHandle viewId, uint32_t resourceId) const;
|
||||
|
||||
bool isReady() const { return mServer; }
|
||||
|
||||
@@ -70,13 +129,28 @@ private:
|
||||
uint32_t mViewCounter = 0;
|
||||
mutable utils::Mutex mViewsMutex;
|
||||
|
||||
struct ResourceMonitor {
|
||||
ViewHandle viewId;
|
||||
uint32_t resourceId;
|
||||
bool enabled = false;
|
||||
bool inProgress = false;
|
||||
size_t readbackTick = 0; // Marks the next tick where an actual readback will be issued.
|
||||
// This relaxes the amount of read back requests per-frame.
|
||||
utils::CString name;
|
||||
std::vector<uint8_t> lastImage;
|
||||
};
|
||||
std::unordered_map<uint64_t, ResourceMonitor> mMonitoredResources;
|
||||
mutable utils::Mutex mMonitoredResourcesMutex;
|
||||
|
||||
ReadbackRequest mReadbackRequest;
|
||||
|
||||
size_t mTickCount = 0;
|
||||
|
||||
class FileRequestHandler* mFileHandler = nullptr;
|
||||
class ApiHandler* mApiHandler = nullptr;
|
||||
class WebSocketHandler* mWebSocketHandler = nullptr;
|
||||
|
||||
friend class FileRequestHandler;
|
||||
friend class ApiHandler;
|
||||
friend class WebSocketHandler;
|
||||
};
|
||||
|
||||
} // namespace filament::fgviewer
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
namespace filament::fgviewer {
|
||||
using ResourceId = uint32_t;
|
||||
|
||||
constexpr const char READBACK_PASS_NAME[] = "Readback Pass";
|
||||
constexpr const char RESOLVED_MONITOR_PASS_NAME[] = "Resolved Monitor";
|
||||
|
||||
class FrameGraphInfo {
|
||||
public:
|
||||
explicit FrameGraphInfo(utils::CString viewName);
|
||||
|
||||
@@ -27,11 +27,12 @@
|
||||
|
||||
#include <CivetServer.h>
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace filament::fgviewer {
|
||||
|
||||
@@ -57,6 +58,14 @@ bool ApiHandler::handleGet(CivetServer* server, struct mg_connection* conn) {
|
||||
return handleGetStatus(conn, request);
|
||||
}
|
||||
|
||||
if (uri.find("/api/image") == 0) {
|
||||
return handleGetImage(conn, request);
|
||||
}
|
||||
|
||||
if (uri == "/api/monitor") {
|
||||
return handleGetMonitor(conn, request);
|
||||
}
|
||||
|
||||
if (uri == "/api/framegraphs") {
|
||||
std::unique_lock const lock(mServer->mViewsMutex);
|
||||
mg_printf(conn, kSuccessHeader.data(), "application/json");
|
||||
@@ -78,13 +87,20 @@ bool ApiHandler::handleGet(CivetServer* server, struct mg_connection* conn) {
|
||||
}
|
||||
|
||||
if (uri == "/api/framegraph") {
|
||||
const FrameGraphInfo* result = getFrameGraphInfo(request);
|
||||
if (!result) {
|
||||
size_t const qlength = strlen(request->query_string);
|
||||
char fgid[9] = {};
|
||||
if (mg_get_var(request->query_string, qlength, "fgid", fgid, sizeof(fgid)) < 0) {
|
||||
return error(__LINE__, uri);
|
||||
}
|
||||
|
||||
uint32_t const id = strtoul(fgid, nullptr, 16);
|
||||
std::unique_lock const lock(mServer->mViewsMutex);
|
||||
auto const it = mServer->mViews.find(id);
|
||||
if (it == mServer->mViews.end()) {
|
||||
return error(__LINE__, uri);
|
||||
}
|
||||
FrameGraphInfo const& result = it->second;
|
||||
JsonWriter writer;
|
||||
if (!writer.writeFrameGraphInfo(*result)) {
|
||||
if (!writer.writeFrameGraphInfo(result)) {
|
||||
return error(__LINE__, uri);
|
||||
}
|
||||
mg_printf(conn, kSuccessHeader.data(), "application/json");
|
||||
@@ -95,25 +111,125 @@ bool ApiHandler::handleGet(CivetServer* server, struct mg_connection* conn) {
|
||||
return error(__LINE__, uri);
|
||||
}
|
||||
|
||||
void ApiHandler::updateFrameGraph(ViewHandle view_handle) {
|
||||
std::unique_lock const lock(mStatusMutex);
|
||||
snprintf(statusFrameGraphId, sizeof(statusFrameGraphId), "%8.8x", view_handle);
|
||||
mCurrentStatus++;
|
||||
mStatusCondition.notify_all();
|
||||
bool ApiHandler::handlePost(CivetServer* server, struct mg_connection* conn) {
|
||||
struct mg_request_info const* request = mg_get_request_info(conn);
|
||||
std::string const& uri = request->local_uri;
|
||||
if (uri == "/api/monitor") {
|
||||
return handlePostMonitor(conn, request);
|
||||
}
|
||||
if (uri == "/api/monitor/clear") {
|
||||
return handlePostMonitorClear(conn, request);
|
||||
}
|
||||
|
||||
return error(__LINE__, uri);
|
||||
}
|
||||
|
||||
const FrameGraphInfo* ApiHandler::getFrameGraphInfo(struct mg_request_info const* request) {
|
||||
size_t const qlength = strlen(request->query_string);
|
||||
char fgid[9] = {};
|
||||
if (mg_get_var(request->query_string, qlength, "fgid", fgid, sizeof(fgid)) < 0) {
|
||||
return nullptr;
|
||||
bool ApiHandler::handlePostMonitor(struct mg_connection* conn,
|
||||
struct mg_request_info const* request) {
|
||||
char postData[1024];
|
||||
int const postDataLen = mg_read(conn, postData, sizeof(postData) - 1);
|
||||
if (postDataLen < 0) {
|
||||
LOG(ERROR) << "mg_read failed";
|
||||
return error(__LINE__, request->local_uri);
|
||||
}
|
||||
uint32_t const id = strtoul(fgid, nullptr, 16);
|
||||
std::unique_lock const lock(mServer->mViewsMutex);
|
||||
const auto it = mServer->mViews.find(id);
|
||||
return it == mServer->mViews.end()
|
||||
? nullptr
|
||||
: &(it->second);
|
||||
postData[postDataLen] = '\0';
|
||||
|
||||
// Primitive JSON parsing
|
||||
std::string_view body(postData);
|
||||
auto fgidPos = body.find("\"fgid\":");
|
||||
auto idPos = body.find("\"id\":");
|
||||
auto namePos = body.find("\"name\":");
|
||||
auto enabledPos = body.find("\"enabled\":");
|
||||
if (idPos == std::string_view::npos || enabledPos == std::string_view::npos) {
|
||||
LOG(ERROR) << "JSON parsing failed for id/enabled. body=" << body;
|
||||
return error(__LINE__, request->local_uri);
|
||||
}
|
||||
|
||||
uint32_t fgid = 0;
|
||||
if (fgidPos != std::string_view::npos) {
|
||||
auto fgidStart = body.find("\"", fgidPos + 7);
|
||||
if (fgidStart != std::string_view::npos) {
|
||||
fgidStart += 1;
|
||||
auto fgidEnd = body.find("\"", fgidStart);
|
||||
if (fgidEnd != std::string_view::npos) {
|
||||
auto sv = body.substr(fgidStart, fgidEnd - fgidStart);
|
||||
try {
|
||||
fgid = std::stoul(std::string(sv), nullptr, 16);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t id = 0;
|
||||
auto idStart = idPos + 4;
|
||||
while (idStart < body.length() && (body[idStart] == ' ' || body[idStart] == ':')) {
|
||||
idStart++;
|
||||
}
|
||||
auto idEnd = body.find(",", idStart);
|
||||
if (idEnd == std::string_view::npos) idEnd = body.find("}", idStart);
|
||||
try {
|
||||
auto sv = body.substr(idStart, idEnd - idStart);
|
||||
auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), id);
|
||||
if (ec != std::errc()) {
|
||||
LOG(ERROR) << "from_chars failed for id. sv=" << sv;
|
||||
}
|
||||
} catch (...) {
|
||||
LOG(ERROR) << "ID extraction threw an exception";
|
||||
return error(__LINE__, request->local_uri);
|
||||
}
|
||||
|
||||
std::string_view name = "Unknown";
|
||||
if (namePos != std::string_view::npos) {
|
||||
auto nameStart = body.find("\"", namePos + 7);
|
||||
if (nameStart != std::string_view::npos) {
|
||||
nameStart++;
|
||||
auto nameEnd = body.find("\"", nameStart);
|
||||
if (nameEnd != std::string_view::npos) {
|
||||
name = body.substr(nameStart, nameEnd - nameStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool const enabled = body.find("true", enabledPos) != std::string_view::npos;
|
||||
|
||||
LOG(INFO) << "Monitor updated: fgid=" << fgid << " id=" << id << " name=" << name
|
||||
<< " enabled=" << enabled;
|
||||
mServer->setResourceMonitor(fgid, id, utils::CString(name.data(), name.length()), enabled);
|
||||
mg_printf(conn, kSuccessHeader.data(), "application/json");
|
||||
mg_printf(conn, "{ \"status\": \"ok\" }");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApiHandler::handlePostMonitorClear(struct mg_connection* conn,
|
||||
struct mg_request_info const* request) {
|
||||
mServer->clearResourceMonitors();
|
||||
mg_printf(conn, kSuccessHeader.data(), "application/json");
|
||||
mg_printf(conn, "{ \"status\": \"ok\" }");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApiHandler::handleGetMonitor(struct mg_connection* conn,
|
||||
struct mg_request_info const* request) {
|
||||
auto const resources = mServer->getMonitoredResources();
|
||||
mg_printf(conn, kSuccessHeader.data(), "application/json");
|
||||
mg_printf(conn, "[");
|
||||
bool first = true;
|
||||
for (auto const& res : resources) {
|
||||
if (!first) mg_printf(conn, ",");
|
||||
mg_printf(conn, "{\"fgid\":%u, \"id\":%u, \"name\":\"%s\"}",
|
||||
res.viewId, res.resourceId, res.name.c_str());
|
||||
first = false;
|
||||
}
|
||||
mg_printf(conn, "]");
|
||||
return true;
|
||||
}
|
||||
|
||||
void ApiHandler::updateFrameGraph(ViewHandle view) {
|
||||
std::unique_lock const lock(mStatusMutex);
|
||||
snprintf(statusFrameGraphId, sizeof(statusFrameGraphId), "%8.8x", view);
|
||||
mCurrentStatus++;
|
||||
mStatusCondition.notify_all();
|
||||
}
|
||||
|
||||
bool ApiHandler::handleGetStatus(struct mg_connection* conn,
|
||||
@@ -142,4 +258,46 @@ bool ApiHandler::handleGetStatus(struct mg_connection* conn,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApiHandler::handleGetImage(struct mg_connection* conn, struct mg_request_info const* request) {
|
||||
char const* qstr = request->query_string;
|
||||
if (!qstr) return false;
|
||||
|
||||
uint32_t fgid = 0;
|
||||
uint32_t id = 0;
|
||||
|
||||
size_t const qlength = strlen(qstr);
|
||||
|
||||
char fgid_str[16] = {};
|
||||
if (mg_get_var(qstr, qlength, "fgid", fgid_str, sizeof(fgid_str)) > 0) {
|
||||
fgid = static_cast<uint32_t>(std::strtoul(fgid_str, nullptr, 16));
|
||||
}
|
||||
|
||||
char id_str[16] = {};
|
||||
if (mg_get_var(qstr, qlength, "id", id_str, sizeof(id_str)) > 0) {
|
||||
id = static_cast<uint32_t>(std::strtoul(id_str, nullptr, 10));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> pngData = mServer->getImage(fgid, id);
|
||||
if (!pngData.empty()) {
|
||||
mg_printf(conn,
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: image/png\r\n"
|
||||
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n",
|
||||
pngData.size());
|
||||
mg_write(conn, pngData.data(), pngData.size());
|
||||
} else {
|
||||
// Return an empty transparent 1x1 PNG or a 404
|
||||
mg_printf(conn, "HTTP/1.1 404 Not Found\r\n"
|
||||
"Content-Length: 0\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // filament::fgviewer
|
||||
|
||||
@@ -38,15 +38,19 @@ public:
|
||||
: mServer(server) {}
|
||||
~ApiHandler() = default;
|
||||
|
||||
bool handleGet(CivetServer* server, struct mg_connection* conn);
|
||||
bool handleGet(CivetServer* server, struct mg_connection* conn) override;
|
||||
bool handlePost(CivetServer* server, struct mg_connection* conn) override;
|
||||
|
||||
void updateFrameGraph(ViewHandle view_handle);
|
||||
|
||||
private:
|
||||
const FrameGraphInfo* getFrameGraphInfo(struct mg_request_info const* request);
|
||||
bool handlePostMonitor(struct mg_connection* conn, struct mg_request_info const* request);
|
||||
bool handlePostMonitorClear(struct mg_connection* conn, struct mg_request_info const* request);
|
||||
|
||||
bool handleGetMonitor(struct mg_connection* conn, struct mg_request_info const* request);
|
||||
bool handleGetStatus(struct mg_connection* conn,
|
||||
struct mg_request_info const* request);
|
||||
bool handleGetImage(struct mg_connection* conn, struct mg_request_info const* request);
|
||||
|
||||
DebugServer* mServer;
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
#include <fgviewer/FrameGraphInfo.h>
|
||||
|
||||
#include "ApiHandler.h"
|
||||
#include "WebSocketHandler.h"
|
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include <stb_image_write.h>
|
||||
|
||||
#include <CivetServer.h>
|
||||
|
||||
@@ -27,6 +29,7 @@
|
||||
#include <utils/ostream.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -39,6 +42,7 @@
|
||||
|
||||
namespace {
|
||||
std::string const BASE_URL = "libs/fgviewer/web";
|
||||
|
||||
} // anonymous
|
||||
|
||||
#else
|
||||
@@ -55,6 +59,24 @@ struct Asset {
|
||||
|
||||
std::unordered_map<std::string_view, Asset> ASSET_MAP;
|
||||
|
||||
constexpr int READBACK_BUFFER = 16;
|
||||
|
||||
struct _Random {
|
||||
private:
|
||||
std::random_device rd;
|
||||
std::mt19937 gen;
|
||||
std::uniform_int_distribution<int> distr;
|
||||
|
||||
public:
|
||||
_Random()
|
||||
: rd(),
|
||||
gen(rd()),
|
||||
distr(-READBACK_BUFFER / 2, READBACK_BUFFER) {}
|
||||
|
||||
int get() { return distr(gen); }
|
||||
} randGen;
|
||||
|
||||
|
||||
} // anonymous
|
||||
|
||||
#endif // SERVE_FROM_SOURCE_TREE
|
||||
@@ -62,6 +84,25 @@ std::unordered_map<std::string_view, Asset> ASSET_MAP;
|
||||
namespace filament::fgviewer {
|
||||
|
||||
using namespace utils;
|
||||
using PixelDataFormat = DebugServer::PixelDataFormat;
|
||||
|
||||
uint8_t getNumberOfComponents(PixelDataFormat format) {
|
||||
switch (format) {
|
||||
case PixelDataFormat::R:
|
||||
return 1;
|
||||
case PixelDataFormat::RG:
|
||||
return 2;
|
||||
case PixelDataFormat::RGB:
|
||||
return 3;
|
||||
case PixelDataFormat::RGBA:
|
||||
return 4;
|
||||
case PixelDataFormat::L:
|
||||
return 1;
|
||||
case PixelDataFormat::LA:
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string_view const DebugServer::kSuccessHeader =
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: %s\r\n"
|
||||
@@ -105,7 +146,8 @@ private:
|
||||
DebugServer* mServer;
|
||||
};
|
||||
|
||||
DebugServer::DebugServer(int port) {
|
||||
DebugServer::DebugServer(int port, ReadbackRequest request)
|
||||
: mReadbackRequest(std::move(request)) {
|
||||
#if !SERVE_FROM_SOURCE_TREE
|
||||
ASSET_MAP["/index.html"] = {
|
||||
.mime = "text/html",
|
||||
@@ -144,7 +186,6 @@ DebugServer::DebugServer(int port) {
|
||||
|
||||
mFileHandler = new FileRequestHandler(this);
|
||||
mApiHandler = new ApiHandler(this);
|
||||
mWebSocketHandler = new WebSocketHandler(this);
|
||||
|
||||
mServer->addHandler("/api", mApiHandler);
|
||||
mServer->addHandler("", mFileHandler);
|
||||
@@ -157,7 +198,6 @@ DebugServer::~DebugServer() {
|
||||
|
||||
delete mFileHandler;
|
||||
delete mApiHandler;
|
||||
delete mWebSocketHandler;
|
||||
delete mServer;
|
||||
}
|
||||
|
||||
@@ -175,7 +215,7 @@ void DebugServer::destroyView(ViewHandle h) {
|
||||
mViews.erase(h);
|
||||
}
|
||||
|
||||
void DebugServer::update(ViewHandle h, FrameGraphInfo info) {
|
||||
void DebugServer::update(ViewHandle h, FrameGraphInfo&& info) {
|
||||
std::unique_lock<utils::Mutex> lock(mViewsMutex);
|
||||
const auto it = mViews.find(h);
|
||||
if (it == mViews.end()) {
|
||||
@@ -183,13 +223,218 @@ void DebugServer::update(ViewHandle h, FrameGraphInfo info) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool has_changed = !(it->second == info);
|
||||
if (!has_changed)
|
||||
bool const hasChanged = !(it->second == info);
|
||||
if (!hasChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
mViews.erase(h);
|
||||
mViews.emplace(h, std::move(info));
|
||||
mApiHandler->updateFrameGraph(h);
|
||||
}
|
||||
|
||||
void DebugServer::tick() {
|
||||
std::unique_lock<utils::Mutex> lock(mMonitoredResourcesMutex);
|
||||
for (auto& [key, monitor]: mMonitoredResources) {
|
||||
if (!monitor.enabled || monitor.inProgress || monitor.readbackTick > mTickCount) {
|
||||
continue;
|
||||
}
|
||||
monitor.readbackTick = mTickCount + READBACK_BUFFER + randGen.get();
|
||||
monitor.inProgress = true;
|
||||
auto name = monitor.name;
|
||||
auto viewId = monitor.viewId;
|
||||
auto id = monitor.resourceId;
|
||||
mReadbackRequest(viewId, id, name,
|
||||
[this, key, id, name](PixelBuffer buffer, uint32_t width, uint32_t height,
|
||||
PixelDataFormat format, FormatInfo info) {
|
||||
if (!buffer.data() || width == 0 || height == 0) {
|
||||
std::unique_lock<utils::Mutex> lock(mMonitoredResourcesMutex);
|
||||
mMonitoredResources[key].inProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
size_t const pixelCount = width * height;
|
||||
PixelBuffer converted(pixelCount * 4);
|
||||
uint8_t* dst = converted.data();
|
||||
|
||||
if (info.isDepth) {
|
||||
// Depth Buffer Normalization
|
||||
// Transforms high-precision floating point or integer depth buffers
|
||||
// (e.g. D16, D24, D32F) into a normalized 0-255 spectrum using an automatic
|
||||
// min/max mapping. Needed to make the micro-variations in Z-depth visibly
|
||||
// distinguishable in the UI.
|
||||
std::vector<float> depthFloats(pixelCount);
|
||||
|
||||
if (info.isD32F) {
|
||||
const float* src = reinterpret_cast<const float*>(buffer.data());
|
||||
for (size_t i = 0; i < pixelCount; ++i) {
|
||||
depthFloats[i] = src[i];
|
||||
}
|
||||
} else if (info.isD16) {
|
||||
const uint16_t* src = reinterpret_cast<const uint16_t*>(buffer.data());
|
||||
for (size_t i = 0; i < pixelCount; ++i) {
|
||||
depthFloats[i] = static_cast<float>(src[i]) / 65535.0f;
|
||||
}
|
||||
} else {
|
||||
const uint32_t* src = reinterpret_cast<const uint32_t*>(buffer.data());
|
||||
for (size_t i = 0; i < pixelCount; ++i) {
|
||||
uint32_t raw = src[i];
|
||||
if (info.isD24) {
|
||||
uint32_t d24 = raw & 0x00FFFFFF;
|
||||
depthFloats[i] = static_cast<float>(d24) / 16777215.0f;
|
||||
} else {
|
||||
depthFloats[i] = static_cast<float>(raw) / 4294967295.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float minVal = std::numeric_limits<float>::max();
|
||||
float maxVal = std::numeric_limits<float>::lowest();
|
||||
for (size_t i = 0; i < pixelCount; ++i) {
|
||||
float v = depthFloats[i];
|
||||
if (v < minVal) minVal = v;
|
||||
if (v > maxVal) maxVal = v;
|
||||
}
|
||||
|
||||
float range = maxVal - minVal;
|
||||
if (range < 1e-6f) range = 1.0f;
|
||||
|
||||
for (size_t i = 0; i < pixelCount; ++i) {
|
||||
float val = (depthFloats[i] - minVal) / range;
|
||||
val = std::pow(val, 0.5f); // simple gamma curve
|
||||
uint8_t c = static_cast<uint8_t>(std::clamp(val, 0.0f, 1.0f) * 255.0f);
|
||||
dst[i * 4 + 0] = c;
|
||||
dst[i * 4 + 1] = c;
|
||||
dst[i * 4 + 2] = c;
|
||||
dst[i * 4 + 3] = 255;
|
||||
}
|
||||
} else if (info.samples > 1) {
|
||||
// MSAA Downsampling
|
||||
// Converts multi-sampled buffers into standard 1x buffers by skipping over
|
||||
// the extra samples. Needed to prevent the UI image from appearing
|
||||
// stretched/distorted by incorrect pitch.
|
||||
const uint8_t* src = reinterpret_cast<const uint8_t*>(buffer.data());
|
||||
for (size_t i = 0; i < pixelCount; ++i) {
|
||||
size_t srcIdx = i * 4 * info.samples;
|
||||
dst[i * 4 + 0] = src[srcIdx + 0];
|
||||
dst[i * 4 + 1] = src[srcIdx + 1];
|
||||
dst[i * 4 + 2] = src[srcIdx + 2];
|
||||
dst[i * 4 + 3] = src[srcIdx + 3];
|
||||
}
|
||||
} else {
|
||||
if (info.isFloat) {
|
||||
// HDR Tonemapping & Exposure
|
||||
// Applies a basic curve to float color targets (RGB16F, R11G11B10F,
|
||||
// RGB9E5). Needed because raw IEEE float bytes cannot be displayed
|
||||
// natively, and HDR values typically exceed 1.0 intensity which
|
||||
// requires mapping back into sRGB 0-255 boundaries.
|
||||
const float* src = reinterpret_cast<const float*>(buffer.data());
|
||||
for (size_t i = 0; i < pixelCount; ++i) {
|
||||
float r = src[i * 4 + 0];
|
||||
float g = src[i * 4 + 1];
|
||||
float b = src[i * 4 + 2];
|
||||
|
||||
dst[i * 4 + 0] = static_cast<uint8_t>(
|
||||
std::pow(std::clamp(r, 0.0f, 1.0f), 1.0f / 2.2f) * 255.0f);
|
||||
dst[i * 4 + 1] = static_cast<uint8_t>(
|
||||
std::pow(std::clamp(g, 0.0f, 1.0f), 1.0f / 2.2f) * 255.0f);
|
||||
dst[i * 4 + 2] = static_cast<uint8_t>(
|
||||
std::pow(std::clamp(b, 0.0f, 1.0f), 1.0f / 2.2f) * 255.0f);
|
||||
dst[i * 4 + 3] = 255;
|
||||
}
|
||||
} else if (info.isR8) {
|
||||
// Single-Channel Grayscale Expand
|
||||
// Copies the Red channel into Green and Blue for visualization.
|
||||
// Needed because raw 8-bit monochromatic targets would otherwise be
|
||||
// parsed as standard RGBA by the PNG encoder, resulting in a garbled
|
||||
// red tint or alignment skew.
|
||||
const uint8_t* src = buffer.data();
|
||||
for (size_t i = 0; i < pixelCount * 4; i += 4) {
|
||||
uint8_t r = src[i];
|
||||
dst[i + 0] = r;
|
||||
dst[i + 1] = r;
|
||||
dst[i + 2] = r;
|
||||
dst[i + 3] = 255;
|
||||
}
|
||||
} else {
|
||||
// Standard 8-bit Buffer
|
||||
// Force alpha to 255 (opaque) since UI should visualize the RGB
|
||||
// contents even if the render pass outputs A=0 logic.
|
||||
const uint8_t* src = buffer.data();
|
||||
for (size_t i = 0; i < pixelCount * 4; i += 4) {
|
||||
dst[i + 0] = src[i + 0];
|
||||
dst[i + 1] = src[i + 1];
|
||||
dst[i + 2] = src[i + 2];
|
||||
dst[i + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int const components = getNumberOfComponents(format);
|
||||
int pngSize = 0;
|
||||
unsigned char* pngData = stbi_write_png_to_mem(converted.data(),
|
||||
width * components, width, height, components, &pngSize);
|
||||
|
||||
if (pngData) {
|
||||
std::unique_lock<utils::Mutex> lock(mMonitoredResourcesMutex);
|
||||
mMonitoredResources[key].lastImage.assign(pngData, pngData + pngSize);
|
||||
free(pngData);
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock<utils::Mutex> lock(mMonitoredResourcesMutex);
|
||||
mMonitoredResources[key].inProgress = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mTickCount++;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> DebugServer::getImage(ViewHandle viewId, uint32_t resourceId) const {
|
||||
std::unique_lock<utils::Mutex> lock(mMonitoredResourcesMutex);
|
||||
uint64_t key = ((uint64_t) viewId << 32) | resourceId;
|
||||
auto it = mMonitoredResources.find(key);
|
||||
if (it != mMonitoredResources.end()) {
|
||||
return it->second.lastImage;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void DebugServer::clearResourceMonitors() {
|
||||
std::unique_lock<utils::Mutex> lock(mMonitoredResourcesMutex);
|
||||
for (auto& [key, monitor] : mMonitoredResources) {
|
||||
monitor.enabled = false;
|
||||
monitor.inProgress = false;
|
||||
monitor.lastImage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<DebugServer::ActiveMonitor> DebugServer::getMonitoredResources() const {
|
||||
std::vector<ActiveMonitor> out;
|
||||
std::unique_lock<utils::Mutex> lock(mMonitoredResourcesMutex);
|
||||
for (auto const& [key, monitor] : mMonitoredResources) {
|
||||
if (monitor.enabled) {
|
||||
out.push_back({monitor.viewId, monitor.resourceId, monitor.name});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void DebugServer::setResourceMonitor(ViewHandle viewId, uint32_t resourceId,
|
||||
utils::CString const& resourceName, bool enabled) {
|
||||
LOG(INFO) << "[fgviewer] DebugServer(" << this << ")::setResourceMonitor: view=" << viewId
|
||||
<< " id=" << resourceId << ", " << resourceName.c_str() << " -> "
|
||||
<< (enabled ? "enabled" : "disabled");
|
||||
std::unique_lock<utils::Mutex> lock(mMonitoredResourcesMutex);
|
||||
uint64_t key = ((uint64_t) viewId << 32) | resourceId;
|
||||
mMonitoredResources[key].viewId = viewId;
|
||||
mMonitoredResources[key].resourceId = resourceId;
|
||||
mMonitoredResources[key].enabled = enabled;
|
||||
mMonitoredResources[key].name = resourceName;
|
||||
if (!enabled) {
|
||||
mMonitoredResources[key].inProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace filament::fgviewer
|
||||
|
||||
@@ -46,7 +46,7 @@ FrameGraphInfo::Pass::Pass(uint32_t id, utils::CString name, std::vector<Resourc
|
||||
renderTargets(std::move(renderTargets)) {}
|
||||
|
||||
bool FrameGraphInfo::Pass::operator==(const Pass& rhs) const {
|
||||
return id == rhs.id && name == rhs.name && reads == rhs.reads && writes == rhs.writes &&
|
||||
return name == rhs.name && reads == rhs.reads && writes == rhs.writes &&
|
||||
renderTargets == rhs.renderTargets;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,11 @@ namespace filament::fgviewer {
|
||||
namespace {
|
||||
|
||||
void writeJSONString(std::ostream& os, const char* str) {
|
||||
if (!str) {
|
||||
os << "\"\"";
|
||||
return;
|
||||
}
|
||||
|
||||
os << '"';
|
||||
const char* p = str;
|
||||
while (*p != '\0') {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "WebSocketHandler.h"
|
||||
|
||||
#include <fgviewer/DebugServer.h>
|
||||
|
||||
#include <utils/Logger.h>
|
||||
|
||||
namespace filament::fgviewer {
|
||||
|
||||
using namespace utils;
|
||||
|
||||
WebSocketHandler::WebSocketHandler(DebugServer* server)
|
||||
: mServer(server) {}
|
||||
|
||||
bool WebSocketHandler::handleConnection(CivetServer* server, const struct mg_connection* conn) {
|
||||
LOG(INFO) << "[fgviewer] WebSocket connected.";
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketHandler::handleReadyState(CivetServer* server, struct mg_connection* conn) {
|
||||
std::unique_lock<utils::Mutex> lock(mMutex);
|
||||
mConnections.insert(conn);
|
||||
}
|
||||
|
||||
bool WebSocketHandler::handleData(CivetServer* server, struct mg_connection* conn, int bits,
|
||||
char* data, size_t data_len) {
|
||||
// For now, we don't expect any data from the client.
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketHandler::handleClose(CivetServer* server, const struct mg_connection* conn) {
|
||||
LOG(INFO) << "[fgviewer] WebSocket disconnected.";
|
||||
std::unique_lock<utils::Mutex> lock(mMutex);
|
||||
mConnections.erase(const_cast<struct mg_connection*>(conn));
|
||||
}
|
||||
|
||||
void WebSocketHandler::broadcast(const char* data, size_t len) {
|
||||
std::unique_lock<utils::Mutex> lock(mMutex);
|
||||
for (auto* conn: mConnections) {
|
||||
mg_websocket_write(conn, MG_WEBSOCKET_OPCODE_BINARY, data, len);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace filament::fgviewer
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FGVIEWER_WEBSOCKET_HANDLER_H
|
||||
#define FGVIEWER_WEBSOCKET_HANDLER_H
|
||||
|
||||
#include <CivetServer.h>
|
||||
|
||||
#include <unordered_set>
|
||||
#include <utils/Mutex.h>
|
||||
|
||||
namespace filament::fgviewer {
|
||||
|
||||
class DebugServer;
|
||||
|
||||
class WebSocketHandler : public CivetWebSocketHandler {
|
||||
public:
|
||||
explicit WebSocketHandler(DebugServer* server);
|
||||
|
||||
bool handleConnection(CivetServer* server, const struct mg_connection* conn) override;
|
||||
void handleReadyState(CivetServer* server, struct mg_connection* conn) override;
|
||||
bool handleData(CivetServer* server, struct mg_connection* conn, int bits, char* data,
|
||||
size_t data_len) override;
|
||||
void handleClose(CivetServer* server, const struct mg_connection* conn) override;
|
||||
|
||||
void broadcast(const char* data, size_t len);
|
||||
|
||||
private:
|
||||
DebugServer const* mServer;
|
||||
utils::Mutex mMutex;
|
||||
std::unordered_set<struct mg_connection*> mConnections;
|
||||
};
|
||||
|
||||
} // namespace filament::fgviewer
|
||||
|
||||
#endif // FGVIEWER_WEBSOCKET_HANDLER_H
|
||||
@@ -41,6 +41,26 @@ async function fetchFrameGraph(fgid) {
|
||||
return fgInfo;
|
||||
}
|
||||
|
||||
async function fetchMonitoredResources() {
|
||||
return await _fetchJson(`api/monitor`);
|
||||
}
|
||||
|
||||
async function clearMonitoredResources() {
|
||||
const response = await fetch(`api/monitor/clear`, {
|
||||
method: "POST"
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function toggleMonitor(fgid, id, name, enabled) {
|
||||
const response = await fetch(`api/monitor`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ fgid: fgid, id: id, name: name, enabled: enabled })
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
const STATUS_LOOP_TIMEOUT = 3000;
|
||||
|
||||
const STATUS_CONNECTED = 1;
|
||||
@@ -55,9 +75,9 @@ async function statusLoop(isConnected, onStatus) {
|
||||
const fgid = await _fetchText("api/status" + (isConnected() ? '' : '?firstTime'));
|
||||
// A first-time request returned successfully
|
||||
if (fgid === '0') {
|
||||
onStatus(STATUS_CONNECTED);
|
||||
await onStatus(STATUS_CONNECTED);
|
||||
} else if (fgid !== '1') {
|
||||
onStatus(STATUS_FRAMEGRAPH_UPDATED, fgid);
|
||||
await onStatus(STATUS_FRAMEGRAPH_UPDATED, fgid);
|
||||
} // fgid == '1' is no-op, just loop again
|
||||
statusLoop(isConnected, onStatus);
|
||||
} catch {
|
||||
|
||||
@@ -51,6 +51,8 @@ const IS_SUBRESOURCE_KEY = 'is_subresource_of'
|
||||
const VIEW_MODE_TABLE = 'table';
|
||||
const VIEW_MODE_GRAPHVIZ = 'graphviz';
|
||||
|
||||
const MONITORED_REFRESH_RATE = 10;
|
||||
|
||||
const COLORS = [
|
||||
"#FFB3BA",
|
||||
"#BAFFC9",
|
||||
@@ -558,6 +560,7 @@ class FrameGraphTable extends LitElement {
|
||||
frameGraphData: {type: Object, state: true}, // Expecting a JSON frame graph structure
|
||||
selectedResourceId: {type: Number, attribute: 'selected-resource'},
|
||||
selectedPassIndex: {type: Number, attribute: 'selected-pass'},
|
||||
monitoredResources: {type: Array, state: true},
|
||||
tooltipText: {type: String, state: true},
|
||||
tooltipVisible: {type: Boolean, state: true},
|
||||
tooltipX: {type: Number, state: true},
|
||||
@@ -565,11 +568,44 @@ class FrameGraphTable extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
_toggleMonitor(resource) {
|
||||
const enabled = !this.monitoredResources.some(r => r.id === resource.id);
|
||||
toggleMonitor(this.selectedFrameGraph, resource.id, resource.name, enabled).then(() => {
|
||||
// Update local state immediately upon success
|
||||
if (!enabled) {
|
||||
const idx = this.monitoredResources.findIndex(r => r.id === resource.id);
|
||||
if (idx >= 0) {
|
||||
const newResources = [...this.monitoredResources];
|
||||
newResources.splice(idx, 1);
|
||||
this.monitoredResources = newResources;
|
||||
|
||||
// Fire custom event to update parent state
|
||||
this.dispatchEvent(new CustomEvent('update-monitored', {
|
||||
detail: newResources,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// If enabling, we'll let the polling loop populate the image url.
|
||||
// But we can add a placeholder so the checkbox immediately checks.
|
||||
const newResources = [...this.monitoredResources, { id: resource.id, name: resource.name, url: '' }];
|
||||
this.monitoredResources = newResources;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('update-monitored', {
|
||||
detail: newResources,
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.frameGraphData = null;
|
||||
this.selectedResourceId = -1;
|
||||
this.selectedPassIndex = -1;
|
||||
this.monitoredResources = [];
|
||||
this.expandedResourceSet = new Set();
|
||||
this.subresourceToParent = {};
|
||||
this._hoverTimeout = null;
|
||||
@@ -764,6 +800,10 @@ class FrameGraphTable extends LitElement {
|
||||
</span>`
|
||||
: nothing}
|
||||
${resource.name}
|
||||
<input type="checkbox"
|
||||
.checked="${this.monitoredResources.some(r => r.id === resource.id)}"
|
||||
@change="${(e) => { e.stopPropagation(); this._toggleMonitor(resource); }}"
|
||||
title="Monitor this buffer">
|
||||
${hasSubresources && !isExpanded ? html`(${subresourceIds.length})` : nothing}
|
||||
</div>
|
||||
${this._renderResourceUsage(allPasses, resourceIds, defaultColor, lastRow)}
|
||||
@@ -865,12 +905,19 @@ class GraphvizView extends LitElement {
|
||||
return;
|
||||
|
||||
try {
|
||||
const viz = d3.select(container)
|
||||
.graphviz({ useWorker: false })
|
||||
.zoom(true)
|
||||
.fit(true);
|
||||
if (!this._viz) {
|
||||
this._viz = d3.select(container)
|
||||
.graphviz({ useWorker: false })
|
||||
.zoom(true)
|
||||
.fit(true);
|
||||
}
|
||||
|
||||
viz.renderDot(this.graphvizData);
|
||||
if (this._lastGraphvizData === this.graphvizData) {
|
||||
return;
|
||||
}
|
||||
this._lastGraphvizData = this.graphvizData;
|
||||
|
||||
this._viz.renderDot(this.graphvizData);
|
||||
} catch (error) {
|
||||
console.error('Failed to render graphviz:', error);
|
||||
container.innerHTML = `<div class="error">Failed to render graphviz: ${error.message}</div>`;
|
||||
@@ -891,6 +938,137 @@ class GraphvizView extends LitElement {
|
||||
|
||||
customElements.define("graphviz-view", GraphvizView);
|
||||
|
||||
class MonitorPanel extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
monitoredResources: { type: Array },
|
||||
expandedResourceId: { type: Number }
|
||||
};
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
pointer-events: none;
|
||||
margin-left: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.monitor-card {
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
pointer-events: auto;
|
||||
width: 125px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monitor-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.monitor-title {
|
||||
margin-bottom: 5px;
|
||||
font-size: 7px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.monitor-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
max-width: 100%;
|
||||
max-height: 80vh;
|
||||
object-fit: contain;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.monitoredResources = [];
|
||||
this.expandedResourceId = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const expandedRes = this.expandedResourceId !== null
|
||||
? this.monitoredResources.find(r => r.id === this.expandedResourceId)
|
||||
: null;
|
||||
|
||||
// If the resource was removed from monitored list while modal is open, close it
|
||||
if (this.expandedResourceId !== null && !expandedRes) {
|
||||
this.expandedResourceId = null;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.monitoredResources.map(res => html`
|
||||
<div class="monitor-card" @click="${() => this.expandedResourceId = res.id}">
|
||||
<div class="monitor-title">
|
||||
<span>${res.name}</span>
|
||||
</div>
|
||||
<img class="monitor-image" src="${res.url}" alt="${res.name}">
|
||||
</div>
|
||||
`)}
|
||||
|
||||
${expandedRes ? html`
|
||||
<div class="modal-overlay" @click="${() => this.expandedResourceId = null}">
|
||||
<div class="modal-content" @click="${e => e.stopPropagation()}">
|
||||
<div class="modal-title">${expandedRes.name}</div>
|
||||
<img class="modal-image" src="${expandedRes.url}" alt="${expandedRes.name}">
|
||||
</div>
|
||||
</div>
|
||||
` : nothing}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("monitor-panel", MonitorPanel);
|
||||
|
||||
class FrameGraphViewer extends LitElement {
|
||||
static get styles() {
|
||||
return css`
|
||||
@@ -923,16 +1101,29 @@ class FrameGraphViewer extends LitElement {
|
||||
this.connected = status == STATUS_CONNECTED || status == STATUS_FRAMEGRAPH_UPDATED;
|
||||
|
||||
if (status == STATUS_FRAMEGRAPH_UPDATED) {
|
||||
let fgInfo = await fetchFrameGraph(fgid);
|
||||
this.database[fgInfo.fgid] = fgInfo;
|
||||
this._framegraphTable.frameGraphData = fgInfo;
|
||||
this._graphvizView.graphvizData = fgInfo.graphviz;
|
||||
let fgInfo = await fetchFrameGraph(fgid);
|
||||
|
||||
this.database[fgid] = fgInfo;
|
||||
this.requestUpdate();
|
||||
|
||||
this._framegraphTable.frameGraphData = fgInfo;
|
||||
// Only update graphviz view if it's visible, else wait for tab switch
|
||||
if (this.viewMode === VIEW_MODE_GRAPHVIZ) {
|
||||
this._graphvizView.graphvizData = fgInfo.graphviz;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let framegraphs = await fetchFrameGraphs();
|
||||
this.database = framegraphs;
|
||||
|
||||
let monitors = await fetchMonitoredResources();
|
||||
this.monitoredResources = monitors.map(m => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
url: `api/image?fgid=${m.fgid}&id=${m.id}&t=${Date.now()}`
|
||||
}));
|
||||
}
|
||||
|
||||
_getFrameGraph() {
|
||||
@@ -949,8 +1140,11 @@ class FrameGraphViewer extends LitElement {
|
||||
this.selectedResourceId = -1;
|
||||
this.selectedPassIndex = -1;
|
||||
this.viewMode = VIEW_MODE_TABLE;
|
||||
this.monitoredResources = [];
|
||||
this.init();
|
||||
|
||||
this._setupImagePolling();
|
||||
|
||||
this.addEventListener('select-framegraph',
|
||||
(ev) => {
|
||||
this.selectedFrameGraph = ev.detail;
|
||||
@@ -970,8 +1164,40 @@ class FrameGraphViewer extends LitElement {
|
||||
);
|
||||
|
||||
this.addEventListener('change-view-mode',
|
||||
(ev) => {
|
||||
async (ev) => {
|
||||
this.viewMode = ev.detail;
|
||||
if (this.viewMode === VIEW_MODE_GRAPHVIZ) {
|
||||
await clearMonitoredResources();
|
||||
this.monitoredResources = [];
|
||||
if (this.selectedFrameGraph) {
|
||||
let fgInfo = await fetchFrameGraph(this.selectedFrameGraph);
|
||||
this.database[this.selectedFrameGraph] = fgInfo;
|
||||
this.requestUpdate();
|
||||
this._graphvizView.graphvizData = fgInfo.graphviz;
|
||||
}
|
||||
} else if (this.viewMode === VIEW_MODE_TABLE && this.selectedFrameGraph) {
|
||||
let fgInfo = await fetchFrameGraph(this.selectedFrameGraph);
|
||||
this.database[this.selectedFrameGraph] = fgInfo;
|
||||
this.requestUpdate();
|
||||
this._framegraphTable.frameGraphData = fgInfo;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.addEventListener('update-monitored',
|
||||
(ev) => {
|
||||
// Keep the ordering when updated from child
|
||||
const newResources = ev.detail;
|
||||
const fg = this._getFrameGraph();
|
||||
if (fg && fg.resources) {
|
||||
const resourceOrder = Object.values(fg.resources);
|
||||
newResources.sort((a, b) => {
|
||||
const idxA = resourceOrder.findIndex(r => r.id === a.id);
|
||||
const idxB = resourceOrder.findIndex(r => r.id === b.id);
|
||||
return idxA - idxB;
|
||||
});
|
||||
}
|
||||
this.monitoredResources = newResources;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -984,6 +1210,7 @@ class FrameGraphViewer extends LitElement {
|
||||
selectedResourceId: {type: Number, state: true},
|
||||
selectedPassIndex: {type: Number, state: true},
|
||||
viewMode: {type: String, state: true},
|
||||
monitoredResources: {type: Array, state: true},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,6 +1224,22 @@ class FrameGraphViewer extends LitElement {
|
||||
}
|
||||
|
||||
|
||||
_setupImagePolling() {
|
||||
if (this._pollingInterval) {
|
||||
clearInterval(this._pollingInterval);
|
||||
}
|
||||
this._pollingInterval = setInterval(() => {
|
||||
if (this.monitoredResources.length > 0 && this.selectedFrameGraph) {
|
||||
const ts = Date.now();
|
||||
const newResources = this.monitoredResources.map(r => ({
|
||||
...r,
|
||||
url: `api/image?fgid=${this.selectedFrameGraph}&id=${r.id}&t=${ts}`
|
||||
}));
|
||||
this.monitoredResources = newResources;
|
||||
}
|
||||
}, 1000 / MONITORED_REFRESH_RATE);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<framegraph-sidepanel id="sidepanel"
|
||||
@@ -1007,13 +1250,17 @@ class FrameGraphViewer extends LitElement {
|
||||
view-mode="${this.viewMode}">
|
||||
</framegraph-sidepanel>
|
||||
|
||||
<framegraph-table id="table"
|
||||
style="display: ${this.viewMode === VIEW_MODE_TABLE ? 'block' : 'none'};"
|
||||
?connected="${this.connected}"
|
||||
selected-framegraph="${this.selectedFrameGraph}"
|
||||
selected-resource="${this.selectedResourceId}"
|
||||
selected-pass="${this.selectedPassIndex}">
|
||||
</framegraph-table>
|
||||
<div style="display:flex;flex-direction:column">
|
||||
<framegraph-table id="table"
|
||||
style="display: ${this.viewMode === VIEW_MODE_TABLE ? 'block' : 'none'};"
|
||||
?connected="${this.connected}"
|
||||
selected-framegraph="${this.selectedFrameGraph}"
|
||||
selected-resource="${this.selectedResourceId}"
|
||||
selected-pass="${this.selectedPassIndex}"
|
||||
.monitoredResources="${this.monitoredResources}">
|
||||
</framegraph-table>
|
||||
<monitor-panel .monitoredResources="${this.monitoredResources}"></monitor-panel>
|
||||
</div>
|
||||
|
||||
<graphviz-view id="graphviz"
|
||||
style="display: ${this.viewMode === VIEW_MODE_GRAPHVIZ ? 'block' : 'none'};"
|
||||
|
||||
Reference in New Issue
Block a user