Skip to content
Merged
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
89 changes: 89 additions & 0 deletions docs/validation/linux-external-image-interop-28.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Linux external-image interop contract

Issue: #28. This checkpoint defines the product-neutral Linux memory and
synchronization boundary needed by native presenters. It does not claim a
working Vulkan producer, a successful Skia import, or hardware qualification.

## Delivered boundary

The v3 scene image lease remains the lifetime root. A presenter starts one
`webscene_gpu_image_consumer_v3`, then calls
`webscene_gpu_linux_acquire_shared_v3`. A successful acquisition returns an
immutable descriptor and a `shared_owner` containing close-on-exec duplicates
of every memory-plane and producer-wait file descriptor.

The descriptor carries:

- opaque-FD or DMA-BUF memory type, up to four planes, offsets, strides, DRM
format and modifier;
- exact allocation/generation/content identity, producer timeline/value and
portable image metadata;
- Vulkan format, type, tiling, usage, flags, initial layout, sharing mode,
creation queue-family list, single-sample/mip/layer counts, memory type,
allocation size and dedicated-allocation state;
- 16-byte Vulkan device and driver UUIDs;
- initialized producer/consumer layouts, exclusive queue-family transfer or
explicit concurrent sharing;
- SyncFD or Vulkan opaque-FD semaphore waits, stable ordering-domain identity,
binary value `1`, and positive timeline values.

Acquisition rejects zero UUIDs, undefined layouts, incomplete queue ownership,
invalid memory combinations, invalid plane descriptors, missing producer
completion/waits, and metadata that does not exactly match the retained lease.
It never substitutes CPU pixels. FD duplication is atomic: partial failure
closes all duplicates and publishes no owner. `close(2)` is deliberately not
retried after `EINTR`, because the descriptor number may already have been
consumed and reused.

The presenter imports or duplicates all borrowed handles before calling
`webscene_gpu_linux_release_shared_v3`. Releasing that owner closes only
WebScene's duplicates. It does not complete image consumption. The presenter
must call `webscene_gpu_image_complete_consumer_v3` only after its GPU queue has
retired every read, including failed imports that submitted no GPU work.

## Dawn and Vulkan boundary

The pinned Dawn revision exposes `SharedTextureMemoryDmaBuf`,
`SharedTextureMemoryOpaqueFD`, `SharedFenceSyncFD`,
`SharedFenceVkSemaphoreOpaqueFD`, and Vulkan begin/end image-layout chains.
Those APIs import externally allocated memory and export synchronization from
`EndAccess`. They do not turn an ordinary Dawn-created texture into exportable
external memory.

For that reason, generic `dawn_canvas_images` deliberately remains an
unsupported provider. A concrete Linux producer must allocate exportable
Vulkan images and memory, import those allocations into Dawn, preserve the
device UUID and Vulkan create info, run `BeginAccess`/`EndAccess`, and publish
the returned layout and fence state through `linux_external_image_provider`.
Until that allocator exists, acquisition returns
`WEBSCENE_GPU_LINUX_SHARED_UNSUPPORTED_PROVIDER_V3`.

ANGLE Vulkan/GL interop remains independently unimplemented. No GL/EGL route is
advertised by this contract.

## Authored gates

- C11 ABI size/offset contracts cover all exported views.
- The provider contract covers exact metadata, memory/layout/queue validation,
producer readiness, timeline rules, FD duplication, closure and rollback.
- Dense Linux linking exports all `webscene_gpu_linux_*_v3` symbols through the
existing version script; macOS export lists include the additive symbols for
stable cross-platform loading and return unsupported outside Linux.
- Linux CMake consumers of `webscene_native_engine` receive
`WEBSCENE_GPU_LINUX_SHARED_ABI_VERSION=3`; the installed runtime C header
carries the same version macro and complete ABI.

Per the active fast-throughput policy, these gates were authored but not run.
Only `git diff --check` is expected for this checkpoint. Required promotion
evidence still includes Linux compilation, ABI/symbol checks, real Vulkan/Dawn
imports, X11 and Wayland Skia pixels, device/driver capability manifests,
invalid modifier/UUID/layout cases, resize/device-loss teardown, a 30-minute
FD/residency plateau, and proof of zero CPU readback/upload in ordinary frames.

## Remaining concrete blocker

Implement the external Vulkan allocation provider described above using the
pinned Dawn/Vulkan profile. Hardware support must be negotiated per format,
modifier, handle type and synchronization route. Unsupported routes remain a
specific failure and cannot fall back to CPU presentation under the GPU-image
capability.
15 changes: 14 additions & 1 deletion experiments/WebScene.NativeEngine.Probe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ if(BUILD_TESTING)
target_compile_features(webscene_graphics_dxgi_contract_tests PRIVATE cxx_std_20)
target_include_directories(webscene_graphics_dxgi_contract_tests PRIVATE native)
add_test(NAME webscene_graphics_dxgi_contract_tests COMMAND webscene_graphics_dxgi_contract_tests)
add_executable(webscene_graphics_linux_external_image_contract_tests
tests/graphics_linux_external_image_contract_tests.cpp)
target_compile_features(webscene_graphics_linux_external_image_contract_tests PRIVATE cxx_std_20)
target_include_directories(webscene_graphics_linux_external_image_contract_tests PRIVATE native)
add_test(NAME webscene_graphics_linux_external_image_contract_tests
COMMAND webscene_graphics_linux_external_image_contract_tests)
set_tests_properties(webscene_graphics_linux_external_image_contract_tests PROPERTIES
LABELS "graphics;linux;abi;lifecycle;security" TIMEOUT 10)
add_executable(webscene_graphics_resource_tests tests/graphics_resource_tests.cpp)
target_compile_features(webscene_graphics_resource_tests PRIVATE cxx_std_20)
target_include_directories(webscene_graphics_resource_tests PRIVATE native)
Expand Down Expand Up @@ -294,7 +302,8 @@ add_library(webscene_native_engine ${webscene_runtime_kind}
native/webscene_native_engine.cpp
native/webscene_secure_random.cpp
native/webscene_indexeddb_storage.cpp
native/webscene_profile_storage.cpp)
native/webscene_profile_storage.cpp
native/graphics/linux_gpu_interop.cpp)
if(TARGET webscene_core)
target_link_libraries(webscene_native_engine PRIVATE webscene_core)
else()
Expand All @@ -305,6 +314,10 @@ endif()
target_compile_features(webscene_native_engine PRIVATE cxx_std_20)
target_compile_definitions(webscene_native_engine PRIVATE WEBSCENE_NATIVE_ENGINE_BUILD)
target_include_directories(webscene_native_engine PUBLIC native)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_compile_definitions(webscene_native_engine INTERFACE
WEBSCENE_GPU_LINUX_SHARED_ABI_VERSION=3)
endif()

if(WIN32)
target_link_libraries(webscene_native_engine PRIVATE bcrypt)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#pragma once
#include "owned_image_pool.h"
#include "posix_fd.h"
#include <array>
#include <memory>
#include <vector>

namespace webscene::graphics {
enum class linux_memory_handle : uint32_t { opaque_fd=1, dma_buf=2 };
enum class linux_sync_handle : uint32_t { sync_fd=1, vk_semaphore_opaque_fd=2 };
enum class linux_queue_sharing : uint32_t { exclusive=1, concurrent=2 };

struct linux_external_plane {
int borrowed_fd{-1};
uint64_t offset{};
uint32_t stride{};
};
struct linux_external_wait {
linux_sync_handle handle{};
int borrowed_fd{-1};
uint64_t ordering_domain{};
uint64_t signaled_value{};
bool timeline{};
};
struct linux_external_image_snapshot {
image_metadata metadata{};
linux_memory_handle memory_handle{};
linux_queue_sharing queue_sharing{};
uint64_t allocation_size{};
uint64_t drm_modifier{};
uint32_t drm_format{};
uint32_t memory_type_index{UINT32_MAX};
uint32_t vk_format{};
uint32_t vk_image_type{};
uint32_t vk_tiling{};
uint32_t vk_usage{};
uint32_t vk_create_flags{};
uint32_t vk_sharing_mode{};
int32_t vk_initial_layout{};
uint32_t vk_queue_family_index_count{};
uint32_t sample_count{};
uint32_t mip_level_count{};
uint32_t array_layer_count{};
int32_t producer_layout{};
int32_t consumer_layout{};
uint32_t producer_queue_family{UINT32_MAX};
uint32_t consumer_queue_family{UINT32_MAX};
std::array<uint32_t,4> vk_queue_family_indices{};
std::array<uint8_t,16> device_uuid{};
std::array<uint8_t,16> driver_uuid{};
bool dedicated_allocation{};
bool producer_complete{};
std::vector<linux_external_plane> planes;
std::vector<linux_external_wait> waits;
};

// Implemented only by a storage allocator that owns Linux external memory.
// export_image borrows its FDs for the duration of the call. The ABI adapter
// duplicates every accepted FD before returning to the host.
struct linux_external_image_provider : image_provider_lifetime {
image_provider_kind kind()const noexcept final{return image_provider_kind::linux_external;}
virtual bool export_image(const image_metadata&,linux_external_image_snapshot&)const=0;
};

inline bool same_image_metadata(const image_metadata& a,const image_metadata& b)noexcept{
return a.canvas==b.canvas&&a.allocation==b.allocation
&&a.allocation_generation==b.allocation_generation&&a.content_serial==b.content_serial
&&a.producer_timeline==b.producer_timeline&&a.producer_value==b.producer_value
&&a.width==b.width&&a.height==b.height&&a.format==b.format&&a.alpha==b.alpha
&&a.color_space==b.color_space&&a.orientation==b.orientation;
}
inline bool nonzero_uuid(const std::array<uint8_t,16>& value)noexcept{
for(const auto byte:value)if(byte)return true;
return false;
}
inline bool valid_linux_external_snapshot(const linux_external_image_snapshot& value,
const image_metadata& expected)noexcept{
if(!same_image_metadata(value.metadata,expected)||!nonzero_uuid(value.device_uuid)
||value.planes.empty()||value.planes.size()>4||value.waits.size()>UINT32_MAX
||value.sample_count!=1||value.mip_level_count!=1||value.array_layer_count!=1
||!value.vk_format||value.vk_image_type!=1||!value.vk_usage
||(value.vk_initial_layout!=0&&value.vk_initial_layout!=8)
||value.producer_layout<=0||value.consumer_layout<=0)return false;
if(value.queue_sharing==linux_queue_sharing::exclusive){
if(value.vk_sharing_mode!=0||value.vk_queue_family_index_count
||value.producer_queue_family==UINT32_MAX||value.consumer_queue_family==UINT32_MAX)return false;
}else if(value.queue_sharing==linux_queue_sharing::concurrent){
if(value.vk_sharing_mode!=1||value.vk_queue_family_index_count<2
||value.vk_queue_family_index_count>value.vk_queue_family_indices.size()
||value.producer_queue_family!=UINT32_MAX||value.consumer_queue_family!=UINT32_MAX)return false;
for(uint32_t i=0;i<value.vk_queue_family_index_count;++i){
if(value.vk_queue_family_indices[i]==UINT32_MAX)return false;
for(uint32_t j=0;j<i;++j)
if(value.vk_queue_family_indices[i]==value.vk_queue_family_indices[j])return false;
}
}else return false;
if(!value.allocation_size)return false;
if(value.memory_handle==linux_memory_handle::opaque_fd){
if(value.planes.size()!=1||value.memory_type_index>=32
||value.drm_format||value.drm_modifier)return false;
}else if(value.memory_handle==linux_memory_handle::dma_buf){
if(!value.drm_format||value.memory_type_index!=UINT32_MAX)return false;
}else return false;
for(const auto& plane:value.planes){
if(plane.borrowed_fd<0)return false;
if(value.memory_handle==linux_memory_handle::dma_buf&&!plane.stride)return false;
}
if(value.producer_complete!=value.waits.empty())return false;
for(size_t index=0;index<value.waits.size();++index){
const auto& wait=value.waits[index];
if(wait.borrowed_fd<0||!wait.ordering_domain)return false;
if(wait.handle==linux_sync_handle::sync_fd){
if(wait.timeline||wait.signaled_value!=1)return false;
}else if(wait.handle==linux_sync_handle::vk_semaphore_opaque_fd){
if(wait.timeline?!wait.signaled_value:wait.signaled_value!=1)return false;
}else return false;
for(size_t prior=0;prior<index;++prior)
if(value.waits[prior].ordering_domain==wait.ordering_domain)return false;
}
return true;
}

template<class FdOps> class basic_linux_shared_image_owner {
public:
using owned_fd=unique_posix_fd<FdOps>;
struct plane { owned_fd fd; uint64_t offset{}; uint32_t stride{}; };
struct wait { linux_sync_handle handle{}; owned_fd fd; uint64_t ordering_domain{}; uint64_t value{}; bool timeline{}; };
private:
linux_external_image_snapshot snapshot_;
std::vector<plane> planes_;
std::vector<wait> waits_;
public:
const linux_external_image_snapshot& snapshot()const noexcept{return snapshot_;}
const std::vector<plane>& planes()const noexcept{return planes_;}
const std::vector<wait>& waits()const noexcept{return waits_;}
static std::unique_ptr<basic_linux_shared_image_owner> create(
const owned_image_pool::consumer& consumer){
const auto metadata=consumer.describe();
const auto anchor=consumer.provider();
if(!anchor||anchor->kind()!=image_provider_kind::linux_external)return {};
const auto provider=std::static_pointer_cast<linux_external_image_provider>(anchor);
linux_external_image_snapshot snapshot;
if(!provider->export_image(metadata,snapshot)||!valid_linux_external_snapshot(snapshot,metadata))
throw std::invalid_argument("invalid Linux external image export");
auto result=std::make_unique<basic_linux_shared_image_owner>();
result->planes_.reserve(snapshot.planes.size());
result->waits_.reserve(snapshot.waits.size());
for(const auto& source:snapshot.planes)
result->planes_.push_back({owned_fd::duplicate(source.borrowed_fd),source.offset,source.stride});
for(const auto& source:snapshot.waits)
result->waits_.push_back({source.handle,owned_fd::duplicate(source.borrowed_fd),
source.ordering_domain,source.signaled_value,source.timeline});
// Never retain the provider's borrowed descriptor integers in the
// immutable metadata copy exposed by owner inspection.
snapshot.planes.clear();snapshot.waits.clear();
result->snapshot_=std::move(snapshot);
return result;
}
};
#if defined(__linux__)
using linux_shared_image_owner=basic_linux_shared_image_owner<linux_fd_ops>;
#endif
} // namespace webscene::graphics
Loading
Loading