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:
Powei Feng
2026-04-21 13:30:45 -07:00
committed by GitHub
parent d3475e6dff
commit 2ae4dc4c62
25 changed files with 1436 additions and 239 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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();

View File

@@ -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
}

View File

@@ -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";
}
}

View 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

View 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

View File

@@ -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.

View File

@@ -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>

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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') {

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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'};"