Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 238 additions & 39 deletions Apps/Playground/Scripts/validation_native.js

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions Apps/UnitTests/Source/Tests.NativeEngine.CubeRenderTargets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,94 @@ TEST(NativeEngineCubeRenderTargets, ClearsEachFaceIndependentlyAndPreserves2DDef
}
device.FinishRenderingCurrentFrame();
}

TEST(NativeEngineCubeRenderTargets, RejectsMultisamplingAndZeroFillsFacesBeforeClear)
{
Babylon::Graphics::Device device{g_deviceConfig};
#if defined(USE_NOOP_METAL_DEVICE) || defined(SKIP_RENDER_TESTS)
GTEST_SKIP() << "GPU rendering/readback is unavailable in this test configuration";
#endif
device.StartRenderingCurrentFrame();
Babylon::AppRuntime runtime{};
constexpr uint16_t size = 4;
constexpr uint32_t faceCount = 6;
std::array<uint8_t, size * size * 4 * faceCount> pixels{};
std::promise<void> completed;
auto future = completed.get_future();
runtime.Dispatch([&](Napi::Env env) {
try
{
device.AddToJavaScript(env);
Babylon::Plugins::NativeEngine::Initialize(env);
auto& context = Babylon::Graphics::DeviceContext::GetFromJavaScript(env);
auto frameScope = context.AcquireFrameCompletionScope();
auto engine = env.Global().Get("_native").As<Napi::Object>().Get("Engine").As<Napi::Function>().New({});
auto createTexture = engine.Get("createTexture").As<Napi::Function>();
auto initializeTexture = engine.Get("initializeTexture").As<Napi::Function>();
auto createFrameBuffer = engine.Get("createFrameBuffer").As<Napi::Function>();
auto value = createTexture.Call(engine, {});
env.Global().Set("_testCube", value);
EXPECT_THROW(initializeTexture.Call(engine, {
value, Napi::Number::New(env, size), Napi::Number::New(env, size),
Napi::Boolean::New(env, false), Napi::Number::New(env, bgfx::TextureFormat::RGBA8),
Napi::Boolean::New(env, true), Napi::Boolean::New(env, false),
Napi::Number::New(env, 4), Napi::Boolean::New(env, true)}), Napi::Error);
initializeTexture.Call(engine, {
value, Napi::Number::New(env, size), Napi::Number::New(env, size),
Napi::Boolean::New(env, false), Napi::Number::New(env, bgfx::TextureFormat::RGBA8),
Napi::Boolean::New(env, true), Napi::Boolean::New(env, false),
Napi::Number::New(env, 1), Napi::Boolean::New(env, true)});
auto* cube = value.As<Napi::Pointer<Babylon::Graphics::Texture>>().Get();
EXPECT_THROW(createFrameBuffer.Call(engine, {
value, Napi::Number::New(env, size), Napi::Number::New(env, size),
Napi::Boolean::New(env, false), Napi::Boolean::New(env, false),
Napi::Number::New(env, 4), Napi::Number::New(env, 0)}), Napi::Error);

auto readback = std::make_shared<Babylon::Graphics::Texture>(context);
readback->Create2D(size * faceCount, size, false, 1, bgfx::TextureFormat::RGBA8,
BGFX_TEXTURE_BLIT_DST | BGFX_TEXTURE_READ_BACK);
for (uint16_t face = 0; face < faceCount; ++face)
{
bgfx::TextureRegion destination{};
destination.init(readback->Handle(), face * size, 0, size, size);
bgfx::TextureRegion source{};
source.init(cube->Handle(), 0, 0, size, size);
source.z = face;
source.depth = 1;
context.GetActiveEncoder()->blit(context.AcquireNewViewId(), destination, source);
}
context.ReadTextureAsync(readback->Handle(), gsl::make_span(pixels))
.then(arcana::inline_scheduler, arcana::cancellation::none(), [readback, &completed](arcana::expected<void, std::exception_ptr> result) {
readback->Dispose();
if (result.has_error())
{
completed.set_exception(result.error());
}
else
{
completed.set_value();
}
});
}
catch (const std::exception& ex)
{
completed.set_exception(std::make_exception_ptr(std::runtime_error{ex.what()}));
}
});
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{30};
while (future.wait_for(std::chrono::milliseconds{16}) != std::future_status::ready)
{
if (std::chrono::steady_clock::now() >= deadline)
{
FAIL() << "Cube face readback was not fulfilled within 30s";
}
device.FinishRenderingCurrentFrame();
device.StartRenderingCurrentFrame();
}
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
EXPECT_NO_THROW(future.get());
for (const uint8_t channel : pixels)
{
EXPECT_EQ(channel, 0);
}
device.FinishRenderingCurrentFrame();
}
12 changes: 8 additions & 4 deletions Core/Graphics/Source/Texture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

namespace
{
const bgfx::Memory* GetZeroImageMemory(uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format)
const bgfx::Memory* GetZeroImageMemory(uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format, bool cubeMap)
{
bgfx::TextureInfo info{};
bgfx::calcTextureSize(info, width, height, /*depth*/ 1, /*cubeMap*/ false, hasMips, numLayers, format);
bgfx::calcTextureSize(info, width, height, /*depth*/ 1, cubeMap, hasMips, numLayers, format);
const bgfx::Memory* mem = bgfx::alloc(info.storageSize);
std::memset(mem->data, 0, mem->size);
return mem;
Expand Down Expand Up @@ -83,7 +83,7 @@ namespace Babylon::Graphics
const auto createFlags = nativeTextureHandle == 0 ? flags | BGFX_TEXTURE_BLIT_DST : flags;

// Make sure render targets are filled with 0 : https://registry.khronos.org/webgl/specs/latest/1.0/#TEXIMAGE2D
const auto* mem = nativeTextureHandle == 0 && (flags & BGFX_TEXTURE_RT) ? GetZeroImageMemory(width, height, hasMips, numLayers, format) : nullptr;
const auto* mem = nativeTextureHandle == 0 && (flags & BGFX_TEXTURE_RT) ? GetZeroImageMemory(width, height, hasMips, numLayers, format, false) : nullptr;

m_handle = bgfx::createTexture2D(width, height, hasMips, numLayers, format, createFlags, mem, nativeTextureHandle);
if (!bgfx::isValid(m_handle))
Expand Down Expand Up @@ -124,7 +124,11 @@ namespace Babylon::Graphics
{
Dispose();

m_handle = bgfx::createTextureCube(size, hasMips, numLayers, format, flags);
// WebGL texImage2D(..., null) zero-fills every cube face. A cube created without
// memory is uninitialized, so render targets must upload explicit zeros.
const auto* mem = (flags & BGFX_TEXTURE_RT) ? GetZeroImageMemory(size, size, hasMips, numLayers, format, true) : nullptr;

m_handle = bgfx::createTextureCube(size, hasMips, numLayers, format, flags, mem);
if (!bgfx::isValid(m_handle))
{
throw std::runtime_error{"Failed to create cube texture"};
Expand Down
21 changes: 12 additions & 9 deletions Documentation/AddingNewValidationTests.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,24 @@ In order to add a new test scene, first thing to do is to add a few lines in `Ap
# Readiness and deterministic capture

The Native runner waits for scene readiness, GUI image readiness, and clean material
defines with ready effects in the active camera's render pass. Utility scenes using
the main scene's camera participate in the same check. Inspection restores the
previous render pass, including on errors.
defines with ready effects in every render pass the next frame can use, including an
output render target and each rig camera. Only utility layers that render automatically
participate; manual layers and layers with `shouldRender` false do not. Inspection
restores the previous render pass, including on errors.

The initial readiness wait and its 10-minute timeout cover both the main scene
and associated utility scenes. Their pending model/texture loads do not consume
the subsequent convergence checks.

Readiness polling does not render extra frames or consume `renderCount`. It refreshes
scene render IDs so material readiness is checked again on the next tick. A scene
that still has not converged after 240 waiting render-loop ticks fails explicitly
and follows normal once-only cleanup and suite continuation. Screenshot and
RenderDoc capture indices still count rendered frames only. Failures invalidate
pending screenshot callbacks so they cannot evaluate after cleanup or during the
next scene.
scene render IDs so material readiness is checked again on the next tick. Waiting is
bounded by wall-clock time, not callback count: a fast render loop can exhaust a tick
cap before asynchronous GUI work finishes. A scene that still has not converged after
60 seconds fails explicitly and follows normal once-only cleanup and suite continuation.
Each associated scene is polled before the results are combined, including every render
pass the next frame can use. Screenshot and RenderDoc capture indices still count
rendered frames only. Failures invalidate pending screenshot callbacks so they cannot
evaluate after cleanup or during the next scene.

Each test restores both the seeded `Math.random` function and its seed, so a snippet
that replaces `Math.random` cannot change the sequence used by the next test.
Expand Down
22 changes: 22 additions & 0 deletions Plugins/NativeEngine/Source/NativeEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1658,6 +1658,10 @@ namespace Babylon
const bool srgb = info[6].As<Napi::Boolean>();
const uint32_t samples = info[7].IsUndefined() ? 1 : info[7].As<Napi::Number>().Uint32Value();
const bool isCube = !info[8].IsUndefined() && info[8].As<Napi::Boolean>();
if (isCube && samples > 1)
{
throw Napi::Error::New(info.Env(), "Multisampled cube render targets are not supported");
}
Comment thread
bkaradzic-microsoft marked this conversation as resolved.

auto flags = BGFX_TEXTURE_NONE;
if (renderTarget)
Expand Down Expand Up @@ -2547,6 +2551,10 @@ namespace Babylon
const uint32_t samples = info[5].IsUndefined() ? 1 : info[5].As<Napi::Number>().Uint32Value();
const double layer = info[6].IsUndefined() ? 0 : info[6].As<Napi::Number>().DoubleValue();
const bool isCube = texture != nullptr && texture->IsCube();
if (isCube && samples > 1)
{
throw Napi::Error::New(info.Env(), "Multisampled cube render targets are not supported");
}
if (!std::isfinite(layer) || layer != std::floor(layer) || layer < 0 || layer > (isCube ? 5 : 0))
{
throw Napi::RangeError::New(info.Env(), isCube
Expand Down Expand Up @@ -2595,6 +2603,20 @@ namespace Babylon
Napi::Value NativeEngine::CreateFrameBufferImpl(Napi::Env env, gsl::span<Graphics::Texture* const> colorTextures, uint16_t width, uint16_t height, bool generateStencilBuffer, bool generateDepth, uint32_t samples, uint16_t layer, Graphics::Texture* depthStencilTexture)
{
const bgfx::Caps* caps = bgfx::getCaps();
if (samples > 1)
{
for (Graphics::Texture* texture : colorTextures)
{
if (texture != nullptr && texture->IsCube())
{
throw Napi::Error::New(env, "Multisampled cube render targets are not supported");
}
}
if (depthStencilTexture != nullptr && depthStencilTexture->IsCube())
{
throw Napi::Error::New(env, "Multisampled cube render targets are not supported");
}
}
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
const uint32_t colorCount = static_cast<uint32_t>(colorTextures.size());
// One slot per color attachment, plus a single depth/stencil attachment only when one is
// generated. bgfx caps the total via maxFBAttachments; reject out-of-range counts up front
Expand Down
5 changes: 3 additions & 2 deletions Plugins/NativeMeshopt/Include/Babylon/Plugins/NativeMeshopt.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ namespace Babylon::Plugins::NativeMeshopt
{
// Exposes `_native.decodeMeshopt(source, count, stride, mode, filter?)`, a
// synchronous native replacement for Babylon's WebAssembly meshopt decoder
// (zeux/meshoptimizer). Babylon.js routes its MeshoptCompression to this
// function when it is present.
// (zeux/meshoptimizer). This is a compatibility export: pinned Babylon.js
// 9.21.2 and the current public MeshoptCompression implementation do not
// reference it and use the script-based decoder.
void BABYLON_API Initialize(Napi::Env env);
}
2 changes: 1 addition & 1 deletion Plugins/NativeMeshopt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The plugin is **off by default**. Enable it with `-D BABYLON_NATIVE_PLUGIN_NATIV
## Limitations

- **Decode only.** Encoding is an authoring-time concern that Babylon Native does not exercise.
- **Compatibility entry point.** Babylon.js probes `_native.decodeMeshopt`; this free-function entry point uses the same decoder as `_native.MeshoptCodec.Decode`. The grouped API remains available.
- **Compatibility entry point.** `_native.decodeMeshopt` is a compatibility export. It is not consumed by the pinned Babylon.js 9.21.2 package or the current public `MeshoptCompression` implementation, which use the script-based decoder. The free function uses the same decoder as `_native.MeshoptCodec.Decode`. The grouped API remains available.

## Design

Expand Down
5 changes: 3 additions & 2 deletions Plugins/NativeMeshopt/Source/NativeMeshopt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ namespace Babylon::Plugins::NativeMeshopt
codec.Set("Version", Napi::String::New(env, MeshoptVersionString()));
native.Set("MeshoptCodec", codec);

// Legacy free-function name. Babylon.js feature-probes `_native.decodeMeshopt`, so keep
// this until the JavaScript side moves to `_native.MeshoptCodec`.
// Compatibility free-function name. Pinned Babylon.js 9.21.2 and the current public
// MeshoptCompression implementation do not reference `_native.decodeMeshopt`; both use
// the script-based decoder. Keep the export so older callers share MeshoptCodec.Decode.
native.Set("decodeMeshopt", Napi::Function::New(env, DecodeMeshopt, "decodeMeshopt"));
}
}
Loading