Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ jobs:
ulimit -c unlimited
xvfb-run ./UnitTests

- name: Module Load Test
if: ${{ !inputs.enable-sanitizers }}
run: |
cd build/Linux/Apps/ModuleLoadTest
ulimit -c unlimited
xvfb-run ./ModuleLoadTest

- name: Upload Rendered Pictures
if: always()
uses: actions/upload-artifact@v6
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/build-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
- name: Build UnitTests macOS
run: cmake --build build/macOS --target UnitTests --config RelWithDebInfo

- name: Build ModuleLoadTest macOS
run: cmake --build build/macOS --target ModuleLoadTest --config RelWithDebInfo

- name: Enable Core Dump
run: sudo sysctl -w kern.corefile=%N.core.%P

Expand All @@ -60,6 +63,13 @@ jobs:
ulimit -c unlimited
./UnitTests

- name: Run ModuleLoadTest macOS
if: ${{ !inputs.enable-sanitizers }}
run: |
cd build/macOS/Apps/ModuleLoadTest/RelWithDebInfo
ulimit -c unlimited
./ModuleLoadTest
Comment thread
bghgary marked this conversation as resolved.

- name: Upload UnitTests Core Dumps
if: failure()
uses: actions/upload-artifact@v6
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/build-win32.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ jobs:
cd build\${{ steps.vars.outputs.solution_name }}\Apps\UnitTests\RelWithDebInfo
UnitTests

- name: Module Load Test
if: ${{ !inputs.enable-sanitizers }}
shell: cmd
run: |
cd build\${{ steps.vars.outputs.solution_name }}\Apps\ModuleLoadTest\RelWithDebInfo
ModuleLoadTest

- name: Stage UnitTests Crash Dump
if: failure()
shell: powershell
Expand Down
1 change: 1 addition & 0 deletions Apps/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ endif()

if((WIN32 AND NOT WINDOWS_STORE) OR (APPLE AND NOT IOS AND NOT VISIONOS) OR (UNIX AND NOT ANDROID AND NOT APPLE))
add_subdirectory(UnitTests)
add_subdirectory(ModuleLoadTest)
endif()

if((WIN32 AND NOT WINDOWS_STORE AND GRAPHICS_API STREQUAL D3D11) OR (APPLE AND NOT IOS AND NOT VISIONOS))
Expand Down
47 changes: 47 additions & 0 deletions Apps/ModuleLoadTest/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
set(SOURCES
"Source/App.h"
"Source/App.cpp"
"Source/ModuleSnapshot.h")

if(WIN32 AND NOT WINDOWS_STORE)
list(APPEND SOURCES
"Source/App.Win32.cpp"
"Source/ModuleSnapshot.Win32.cpp")
elseif(APPLE AND NOT IOS)
list(APPEND SOURCES
"Source/App.Apple.mm"
"Source/ModuleSnapshot.macOS.mm")
find_library(JAVASCRIPTCORE_LIBRARY JavaScriptCore)
set(ADDITIONAL_LIBRARIES PRIVATE ${JAVASCRIPTCORE_LIBRARY})
elseif(UNIX AND NOT ANDROID)
list(APPEND SOURCES
"Source/App.X11.cpp"
"Source/ModuleSnapshot.Linux.cpp")
else()
# Unsupported platform (UWP, Android, iOS) — mobile/store bundling
# changes module-load semantics. Silently skip.
return()
endif()

add_executable(ModuleLoadTest ${SOURCES})

target_link_libraries(ModuleLoadTest
PRIVATE AppRuntime
PRIVATE Blob
PRIVATE Canvas
PRIVATE Console
PRIVATE GraphicsDevice
PRIVATE NativeEngine
PRIVATE NativeEncoding
PRIVATE Window
PRIVATE XMLHttpRequest
${ADDITIONAL_LIBRARIES})

add_test(NAME ModuleLoadTest COMMAND ModuleLoadTest CONFIGURATIONS Release RelWithDebInfo)

# See https://gitlab.kitware.com/cmake/cmake/-/issues/23543
add_custom_command(TARGET ModuleLoadTest POST_BUILD
COMMAND ${CMAKE_COMMAND} -E $<IF:$<BOOL:$<TARGET_RUNTIME_DLLS:ModuleLoadTest>>,copy,true> $<TARGET_RUNTIME_DLLS:ModuleLoadTest> $<TARGET_FILE_DIR:ModuleLoadTest> COMMAND_EXPAND_LISTS)

set_property(TARGET ModuleLoadTest PROPERTY FOLDER Apps)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
91 changes: 91 additions & 0 deletions Apps/ModuleLoadTest/Source/App.Apple.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#include "App.h"
#include "ModuleSnapshot.h"

#include <Babylon/DebugTrace.h>
#include <Babylon/Graphics/Device.h>

#import <Metal/Metal.hpp>

#include <sys/sysctl.h>
#include <sys/types.h>
#include <unistd.h>

#include <iostream>
#include <string_view>

#import <Foundation/Foundation.h>

namespace ModuleLoadTest
{
// Apple equivalent of IsDebuggerPresent() — non-invasive.
// https://developer.apple.com/library/archive/qa/qa1361/_index.html
bool IsBeingTraced()
{
int name[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid() };
struct kinfo_proc info{};
size_t size = sizeof(info);
if (sysctl(name, 4, &info, &size, nullptr, 0) != 0)
{
return false;
}
return (info.kp_proc.p_flag & P_TRACED) != 0;
}

const ModuleSnapshot& GetExpectedBootModules()
{
// Seeded from CI on macos-latest (ARM64 paravirtualized GPU runner).
// Append entries here as new configs surface deltas.
static const ModuleSnapshot kModules{
"appleparavirtgpumetaliogpufamily",
"iogpu",
};
return kModules;
}

// On macOS the interesting per-SKU variation is the Metal/GPU driver
// bundles (AMDMTLBronzeDriver, AppleIntelKBLGraphicsMTLDriver, ...), which
// dyld loads by path under /System/Library/Extensions/. Base names still
// differ between Apple Silicon and Intel. Seed a broad carve-out.
bool IsAllowedOptionalModule(std::string_view name)
{
static constexpr std::string_view kPrefixes[] = {
// Metal GPU driver bundles
"amdmtl",
"appleintel",
"applem1",
"applem2",
"applem3",
"nvmtl",
// Ambient/IOSurface layer
"iosurface",
};
for (const auto& prefix : kPrefixes)
{
if (name.size() >= prefix.size() && name.compare(0, prefix.size(), prefix) == 0)
{
return true;
}
}
return false;
}
std::optional<Babylon::Graphics::Configuration> CreateGraphicsConfig()
{
// MTL::Device must outlive RunBoot. Park in function-local static
// storage so it lives for the duration of the process.
static NS::SharedPtr<MTL::Device> device = NS::TransferPtr(MTL::CreateSystemDefaultDevice());
if (!device)
{
std::cout << "ModuleLoadTest: SKIP - no Metal device available." << std::endl;
return std::nullopt;
}

Babylon::DebugTrace::EnableDebugTrace(true);
Babylon::DebugTrace::SetTraceOutput([](const char* trace) { NSLog(@"%s", trace); });

Babylon::Graphics::Configuration config{};
config.Device = device.get();
config.Width = 600;
config.Height = 400;
return config;
}
}
179 changes: 179 additions & 0 deletions Apps/ModuleLoadTest/Source/App.Win32.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#include "App.h"
#include "ModuleSnapshot.h"

#include <Babylon/DebugTrace.h>
#include <Babylon/Graphics/Device.h>

#include <Windows.h>

#include <iostream>
#include <string_view>

namespace ModuleLoadTest
{
namespace
{
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
return ::DefWindowProc(hWnd, msg, wParam, lParam);
}
}

bool IsBeingTraced()
{
return ::IsDebuggerPresent() != FALSE;
}

// Expected set of modules loaded during BabylonNative boot, as a delta
// from the baseline snapshot captured by the TLS callback before any C++
// static initializer in this binary has run. Base names only, lower case.
//
// This list targets optimized builds (Release and RelWithDebInfo, which
// produce identical module sets). Debug builds load additional debug CRT
// and diagnostic DLLs and are not supported — main() returns a SKIP in
// that config. CI runs only in RelWithDebInfo (see CMakeLists.txt's
// `add_test ... CONFIGURATIONS Release RelWithDebInfo`).
//
// Launch-environment noise (e.g. VS's Start Without Debugging injecting
// kernel.appcore.dll) is filtered via IsAllowedOptionalModule so devs
// see the same verdict from Ctrl-F5 as CI does from a plain cmd launch.
//
// The assertion is asymmetric: we FAIL on modules not in this list (a new
// module was pulled in — the regression signal we want), and we IGNORE
// modules in this list that did not load (environmental variance across
// GPU SKUs, Windows patch levels, VS vs cmd launch, etc.). This lets the
// list be a permissive superset that works on both dev machines and CI.
//
// Motivating example: a dependency quietly pulling in dbghelp.dll on boot.
// dbghelp.dll lives in System32, so a path-based OS filter would miss it;
// we therefore golden-list the full delta (including OS modules) and only
// allow a narrow name-pattern carve-out for GPU driver ICDs whose exact
// names differ per runner SKU (see IsAllowedOptionalModule).
const ModuleSnapshot& GetExpectedBootModules()
{
// Seeded from a local RelWithDebInfo run on Windows 11 x64 with D3D11
// and Chakra. CI may add more entries for other Win32 configs
// (V8/JSI/D3D12) — those should be appended as the draft PR runs.
static const ModuleSnapshot kModules{
"bcryptprimitives.dll",
"cfgmgr32.dll",
"crypt32.dll",
"cryptbase.dll",
"cryptnet.dll",
"d3d10warp.dll",
"d3d11.dll",
"d3d11_3sdklayers.dll",
"d3d12.dll",
"d3d12core.dll",
"d3d12sdklayers.dll",
"d3dscache.dll",
// TODO: bgfx loads dbghelp.dll at boot (callstack/crash helper). Drop this
// entry once bgfx stops pulling it in.
"dbghelp.dll",
Comment thread
bghgary marked this conversation as resolved.
"dcomp.dll",
"devobj.dll",
"directxdatabasehelper.dll",
"drvstore.dll",
"dwmapi.dll",
"dxcore.dll",
"dxgi.dll",
"dxgidebug.dll",
"dxilconv.dll",
"iertutil.dll",
"imagehlp.dll",
"msasn1.dll",
"msctf.dll",
"netutils.dll",
"ntmarta.dll",
"powrprof.dll",
"profapi.dll",
"rsaenh.dll",
"setupapi.dll",
"shcore.dll",
"shell32.dll",
"srvcli.dll",
"umpdc.dll",
"userenv.dll",
"uxtheme.dll",
"version.dll",
"windows.storage.dll",
"winmm.dll",
"wintrust.dll",
"wintypes.dll",
"wldp.dll",
};
return kModules;
}

// Name patterns for modules whose presence in the delta is allowed but
// not required. Covers two classes:
//
// * GPU driver ICDs, whose exact base name depends on which GPU the
// runner has (NVIDIA/Intel/AMD/ATI).
// * Environmental/ambient DLLs that some launchers (e.g. Visual Studio's
// Start Without Debugging) inject into the process but which a plain
// cmd.exe launch does not. These are not introduced by BabylonNative.
//
// Prefixes match at the start of the base name; exacts match the whole
// name.
bool IsAllowedOptionalModule(std::string_view name)
{
static constexpr std::string_view kPrefixes[] = {
// GPU driver ICDs
"nv", // NVIDIA (nvoglv64.dll, nvapi64.dll, nvd3dum.dll, ...)
"ig", // Intel (igdumd64.dll, igxelpicd64.dll, ...)
"amd", // AMD (amdvlk64.dll, amdxc64.dll, ...)
"atio", // AMD/ATI (atio6axx.dll, ...)
// Ambient WARP variants
"microsoft.internal.warppal",
};
static constexpr std::string_view kExact[] = {
// GPU driver ICD
"dxil.dll",
// VS Start-Without-Debugging launch environment
"kernel.appcore.dll",
};
for (const auto& prefix : kPrefixes)
{
if (name.size() >= prefix.size() && name.compare(0, prefix.size(), prefix) == 0)
{
return true;
}
}
for (const auto& exact : kExact)
{
if (name == exact)
{
return true;
}
}
return false;
}
std::optional<Babylon::Graphics::Configuration> CreateGraphicsConfig()
{
::SetConsoleOutputCP(CP_UTF8);

// bgfx D3D12 implementation requires an HWND to avoid a device refcount
// leak on shutdown. Create a hidden window to satisfy that requirement.
// Parked in function-local static storage so the handle lives for the
// duration of the process.
static WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, WndProc, 0L, 0L,
::GetModuleHandle(nullptr), nullptr, nullptr, nullptr, nullptr,
"BabylonNativeModuleLoadTest", nullptr };
::RegisterClassEx(&wc);
static HWND hWnd = ::CreateWindow(wc.lpszClassName, "BabylonNativeModuleLoadTest",
WS_OVERLAPPEDWINDOW, -1, -1, -1, -1, nullptr, nullptr, wc.hInstance, nullptr);

Babylon::DebugTrace::EnableDebugTrace(true);
Babylon::DebugTrace::SetTraceOutput([](const char* trace) {
::OutputDebugStringA(trace);
::OutputDebugStringA("\n");
});

Babylon::Graphics::Configuration config{};
config.Window = hWnd;
config.Width = 600;
config.Height = 400;
return config;
}
}
Loading