Skip to content

Commit 2826e02

Browse files
Add Linux external image export ABI (#630)
1 parent 5e5b14a commit 2826e02

14 files changed

Lines changed: 727 additions & 13 deletions
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Linux external-image interop contract
2+
3+
Issue: #28. This checkpoint defines the product-neutral Linux memory and
4+
synchronization boundary needed by native presenters. It does not claim a
5+
working Vulkan producer, a successful Skia import, or hardware qualification.
6+
7+
## Delivered boundary
8+
9+
The v3 scene image lease remains the lifetime root. A presenter starts one
10+
`webscene_gpu_image_consumer_v3`, then calls
11+
`webscene_gpu_linux_acquire_shared_v3`. A successful acquisition returns an
12+
immutable descriptor and a `shared_owner` containing close-on-exec duplicates
13+
of every memory-plane and producer-wait file descriptor.
14+
15+
The descriptor carries:
16+
17+
- opaque-FD or DMA-BUF memory type, up to four planes, offsets, strides, DRM
18+
format and modifier;
19+
- exact allocation/generation/content identity, producer timeline/value and
20+
portable image metadata;
21+
- Vulkan format, type, tiling, usage, flags, initial layout, sharing mode,
22+
creation queue-family list, single-sample/mip/layer counts, memory type,
23+
allocation size and dedicated-allocation state;
24+
- 16-byte Vulkan device and driver UUIDs;
25+
- initialized producer/consumer layouts, exclusive queue-family transfer or
26+
explicit concurrent sharing;
27+
- SyncFD or Vulkan opaque-FD semaphore waits, stable ordering-domain identity,
28+
binary value `1`, and positive timeline values.
29+
30+
Acquisition rejects zero UUIDs, undefined layouts, incomplete queue ownership,
31+
invalid memory combinations, invalid plane descriptors, missing producer
32+
completion/waits, and metadata that does not exactly match the retained lease.
33+
It never substitutes CPU pixels. FD duplication is atomic: partial failure
34+
closes all duplicates and publishes no owner. `close(2)` is deliberately not
35+
retried after `EINTR`, because the descriptor number may already have been
36+
consumed and reused.
37+
38+
The presenter imports or duplicates all borrowed handles before calling
39+
`webscene_gpu_linux_release_shared_v3`. Releasing that owner closes only
40+
WebScene's duplicates. It does not complete image consumption. The presenter
41+
must call `webscene_gpu_image_complete_consumer_v3` only after its GPU queue has
42+
retired every read, including failed imports that submitted no GPU work.
43+
44+
## Dawn and Vulkan boundary
45+
46+
The pinned Dawn revision exposes `SharedTextureMemoryDmaBuf`,
47+
`SharedTextureMemoryOpaqueFD`, `SharedFenceSyncFD`,
48+
`SharedFenceVkSemaphoreOpaqueFD`, and Vulkan begin/end image-layout chains.
49+
Those APIs import externally allocated memory and export synchronization from
50+
`EndAccess`. They do not turn an ordinary Dawn-created texture into exportable
51+
external memory.
52+
53+
For that reason, generic `dawn_canvas_images` deliberately remains an
54+
unsupported provider. A concrete Linux producer must allocate exportable
55+
Vulkan images and memory, import those allocations into Dawn, preserve the
56+
device UUID and Vulkan create info, run `BeginAccess`/`EndAccess`, and publish
57+
the returned layout and fence state through `linux_external_image_provider`.
58+
Until that allocator exists, acquisition returns
59+
`WEBSCENE_GPU_LINUX_SHARED_UNSUPPORTED_PROVIDER_V3`.
60+
61+
ANGLE Vulkan/GL interop remains independently unimplemented. No GL/EGL route is
62+
advertised by this contract.
63+
64+
## Authored gates
65+
66+
- C11 ABI size/offset contracts cover all exported views.
67+
- The provider contract covers exact metadata, memory/layout/queue validation,
68+
producer readiness, timeline rules, FD duplication, closure and rollback.
69+
- Dense Linux linking exports all `webscene_gpu_linux_*_v3` symbols through the
70+
existing version script; macOS export lists include the additive symbols for
71+
stable cross-platform loading and return unsupported outside Linux.
72+
- Linux CMake consumers of `webscene_native_engine` receive
73+
`WEBSCENE_GPU_LINUX_SHARED_ABI_VERSION=3`; the installed runtime C header
74+
carries the same version macro and complete ABI.
75+
76+
Per the active fast-throughput policy, these gates were authored but not run.
77+
Only `git diff --check` is expected for this checkpoint. Required promotion
78+
evidence still includes Linux compilation, ABI/symbol checks, real Vulkan/Dawn
79+
imports, X11 and Wayland Skia pixels, device/driver capability manifests,
80+
invalid modifier/UUID/layout cases, resize/device-loss teardown, a 30-minute
81+
FD/residency plateau, and proof of zero CPU readback/upload in ordinary frames.
82+
83+
## Remaining concrete blocker
84+
85+
Implement the external Vulkan allocation provider described above using the
86+
pinned Dawn/Vulkan profile. Hardware support must be negotiated per format,
87+
modifier, handle type and synchronization route. Unsupported routes remain a
88+
specific failure and cannot fall back to CPU presentation under the GPU-image
89+
capability.

‎experiments/WebScene.NativeEngine.Probe/CMakeLists.txt‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ if(BUILD_TESTING)
240240
target_compile_features(webscene_graphics_dxgi_contract_tests PRIVATE cxx_std_20)
241241
target_include_directories(webscene_graphics_dxgi_contract_tests PRIVATE native)
242242
add_test(NAME webscene_graphics_dxgi_contract_tests COMMAND webscene_graphics_dxgi_contract_tests)
243+
add_executable(webscene_graphics_linux_external_image_contract_tests
244+
tests/graphics_linux_external_image_contract_tests.cpp)
245+
target_compile_features(webscene_graphics_linux_external_image_contract_tests PRIVATE cxx_std_20)
246+
target_include_directories(webscene_graphics_linux_external_image_contract_tests PRIVATE native)
247+
add_test(NAME webscene_graphics_linux_external_image_contract_tests
248+
COMMAND webscene_graphics_linux_external_image_contract_tests)
249+
set_tests_properties(webscene_graphics_linux_external_image_contract_tests PROPERTIES
250+
LABELS "graphics;linux;abi;lifecycle;security" TIMEOUT 10)
243251
add_executable(webscene_graphics_resource_tests tests/graphics_resource_tests.cpp)
244252
target_compile_features(webscene_graphics_resource_tests PRIVATE cxx_std_20)
245253
target_include_directories(webscene_graphics_resource_tests PRIVATE native)
@@ -294,7 +302,8 @@ add_library(webscene_native_engine ${webscene_runtime_kind}
294302
native/webscene_native_engine.cpp
295303
native/webscene_secure_random.cpp
296304
native/webscene_indexeddb_storage.cpp
297-
native/webscene_profile_storage.cpp)
305+
native/webscene_profile_storage.cpp
306+
native/graphics/linux_gpu_interop.cpp)
298307
if(TARGET webscene_core)
299308
target_link_libraries(webscene_native_engine PRIVATE webscene_core)
300309
else()
@@ -305,6 +314,10 @@ endif()
305314
target_compile_features(webscene_native_engine PRIVATE cxx_std_20)
306315
target_compile_definitions(webscene_native_engine PRIVATE WEBSCENE_NATIVE_ENGINE_BUILD)
307316
target_include_directories(webscene_native_engine PUBLIC native)
317+
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
318+
target_compile_definitions(webscene_native_engine INTERFACE
319+
WEBSCENE_GPU_LINUX_SHARED_ABI_VERSION=3)
320+
endif()
308321

309322
if(WIN32)
310323
target_link_libraries(webscene_native_engine PRIVATE bcrypt)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#pragma once
2+
#include "owned_image_pool.h"
3+
#include "posix_fd.h"
4+
#include <array>
5+
#include <memory>
6+
#include <vector>
7+
8+
namespace webscene::graphics {
9+
enum class linux_memory_handle : uint32_t { opaque_fd=1, dma_buf=2 };
10+
enum class linux_sync_handle : uint32_t { sync_fd=1, vk_semaphore_opaque_fd=2 };
11+
enum class linux_queue_sharing : uint32_t { exclusive=1, concurrent=2 };
12+
13+
struct linux_external_plane {
14+
int borrowed_fd{-1};
15+
uint64_t offset{};
16+
uint32_t stride{};
17+
};
18+
struct linux_external_wait {
19+
linux_sync_handle handle{};
20+
int borrowed_fd{-1};
21+
uint64_t ordering_domain{};
22+
uint64_t signaled_value{};
23+
bool timeline{};
24+
};
25+
struct linux_external_image_snapshot {
26+
image_metadata metadata{};
27+
linux_memory_handle memory_handle{};
28+
linux_queue_sharing queue_sharing{};
29+
uint64_t allocation_size{};
30+
uint64_t drm_modifier{};
31+
uint32_t drm_format{};
32+
uint32_t memory_type_index{UINT32_MAX};
33+
uint32_t vk_format{};
34+
uint32_t vk_image_type{};
35+
uint32_t vk_tiling{};
36+
uint32_t vk_usage{};
37+
uint32_t vk_create_flags{};
38+
uint32_t vk_sharing_mode{};
39+
int32_t vk_initial_layout{};
40+
uint32_t vk_queue_family_index_count{};
41+
uint32_t sample_count{};
42+
uint32_t mip_level_count{};
43+
uint32_t array_layer_count{};
44+
int32_t producer_layout{};
45+
int32_t consumer_layout{};
46+
uint32_t producer_queue_family{UINT32_MAX};
47+
uint32_t consumer_queue_family{UINT32_MAX};
48+
std::array<uint32_t,4> vk_queue_family_indices{};
49+
std::array<uint8_t,16> device_uuid{};
50+
std::array<uint8_t,16> driver_uuid{};
51+
bool dedicated_allocation{};
52+
bool producer_complete{};
53+
std::vector<linux_external_plane> planes;
54+
std::vector<linux_external_wait> waits;
55+
};
56+
57+
// Implemented only by a storage allocator that owns Linux external memory.
58+
// export_image borrows its FDs for the duration of the call. The ABI adapter
59+
// duplicates every accepted FD before returning to the host.
60+
struct linux_external_image_provider : image_provider_lifetime {
61+
image_provider_kind kind()const noexcept final{return image_provider_kind::linux_external;}
62+
virtual bool export_image(const image_metadata&,linux_external_image_snapshot&)const=0;
63+
};
64+
65+
inline bool same_image_metadata(const image_metadata& a,const image_metadata& b)noexcept{
66+
return a.canvas==b.canvas&&a.allocation==b.allocation
67+
&&a.allocation_generation==b.allocation_generation&&a.content_serial==b.content_serial
68+
&&a.producer_timeline==b.producer_timeline&&a.producer_value==b.producer_value
69+
&&a.width==b.width&&a.height==b.height&&a.format==b.format&&a.alpha==b.alpha
70+
&&a.color_space==b.color_space&&a.orientation==b.orientation;
71+
}
72+
inline bool nonzero_uuid(const std::array<uint8_t,16>& value)noexcept{
73+
for(const auto byte:value)if(byte)return true;
74+
return false;
75+
}
76+
inline bool valid_linux_external_snapshot(const linux_external_image_snapshot& value,
77+
const image_metadata& expected)noexcept{
78+
if(!same_image_metadata(value.metadata,expected)||!nonzero_uuid(value.device_uuid)
79+
||value.planes.empty()||value.planes.size()>4||value.waits.size()>UINT32_MAX
80+
||value.sample_count!=1||value.mip_level_count!=1||value.array_layer_count!=1
81+
||!value.vk_format||value.vk_image_type!=1||!value.vk_usage
82+
||(value.vk_initial_layout!=0&&value.vk_initial_layout!=8)
83+
||value.producer_layout<=0||value.consumer_layout<=0)return false;
84+
if(value.queue_sharing==linux_queue_sharing::exclusive){
85+
if(value.vk_sharing_mode!=0||value.vk_queue_family_index_count
86+
||value.producer_queue_family==UINT32_MAX||value.consumer_queue_family==UINT32_MAX)return false;
87+
}else if(value.queue_sharing==linux_queue_sharing::concurrent){
88+
if(value.vk_sharing_mode!=1||value.vk_queue_family_index_count<2
89+
||value.vk_queue_family_index_count>value.vk_queue_family_indices.size()
90+
||value.producer_queue_family!=UINT32_MAX||value.consumer_queue_family!=UINT32_MAX)return false;
91+
for(uint32_t i=0;i<value.vk_queue_family_index_count;++i){
92+
if(value.vk_queue_family_indices[i]==UINT32_MAX)return false;
93+
for(uint32_t j=0;j<i;++j)
94+
if(value.vk_queue_family_indices[i]==value.vk_queue_family_indices[j])return false;
95+
}
96+
}else return false;
97+
if(!value.allocation_size)return false;
98+
if(value.memory_handle==linux_memory_handle::opaque_fd){
99+
if(value.planes.size()!=1||value.memory_type_index>=32
100+
||value.drm_format||value.drm_modifier)return false;
101+
}else if(value.memory_handle==linux_memory_handle::dma_buf){
102+
if(!value.drm_format||value.memory_type_index!=UINT32_MAX)return false;
103+
}else return false;
104+
for(const auto& plane:value.planes){
105+
if(plane.borrowed_fd<0)return false;
106+
if(value.memory_handle==linux_memory_handle::dma_buf&&!plane.stride)return false;
107+
}
108+
if(value.producer_complete!=value.waits.empty())return false;
109+
for(size_t index=0;index<value.waits.size();++index){
110+
const auto& wait=value.waits[index];
111+
if(wait.borrowed_fd<0||!wait.ordering_domain)return false;
112+
if(wait.handle==linux_sync_handle::sync_fd){
113+
if(wait.timeline||wait.signaled_value!=1)return false;
114+
}else if(wait.handle==linux_sync_handle::vk_semaphore_opaque_fd){
115+
if(wait.timeline?!wait.signaled_value:wait.signaled_value!=1)return false;
116+
}else return false;
117+
for(size_t prior=0;prior<index;++prior)
118+
if(value.waits[prior].ordering_domain==wait.ordering_domain)return false;
119+
}
120+
return true;
121+
}
122+
123+
template<class FdOps> class basic_linux_shared_image_owner {
124+
public:
125+
using owned_fd=unique_posix_fd<FdOps>;
126+
struct plane { owned_fd fd; uint64_t offset{}; uint32_t stride{}; };
127+
struct wait { linux_sync_handle handle{}; owned_fd fd; uint64_t ordering_domain{}; uint64_t value{}; bool timeline{}; };
128+
private:
129+
linux_external_image_snapshot snapshot_;
130+
std::vector<plane> planes_;
131+
std::vector<wait> waits_;
132+
public:
133+
const linux_external_image_snapshot& snapshot()const noexcept{return snapshot_;}
134+
const std::vector<plane>& planes()const noexcept{return planes_;}
135+
const std::vector<wait>& waits()const noexcept{return waits_;}
136+
static std::unique_ptr<basic_linux_shared_image_owner> create(
137+
const owned_image_pool::consumer& consumer){
138+
const auto metadata=consumer.describe();
139+
const auto anchor=consumer.provider();
140+
if(!anchor||anchor->kind()!=image_provider_kind::linux_external)return {};
141+
const auto provider=std::static_pointer_cast<linux_external_image_provider>(anchor);
142+
linux_external_image_snapshot snapshot;
143+
if(!provider->export_image(metadata,snapshot)||!valid_linux_external_snapshot(snapshot,metadata))
144+
throw std::invalid_argument("invalid Linux external image export");
145+
auto result=std::make_unique<basic_linux_shared_image_owner>();
146+
result->planes_.reserve(snapshot.planes.size());
147+
result->waits_.reserve(snapshot.waits.size());
148+
for(const auto& source:snapshot.planes)
149+
result->planes_.push_back({owned_fd::duplicate(source.borrowed_fd),source.offset,source.stride});
150+
for(const auto& source:snapshot.waits)
151+
result->waits_.push_back({source.handle,owned_fd::duplicate(source.borrowed_fd),
152+
source.ordering_domain,source.signaled_value,source.timeline});
153+
// Never retain the provider's borrowed descriptor integers in the
154+
// immutable metadata copy exposed by owner inspection.
155+
snapshot.planes.clear();snapshot.waits.clear();
156+
result->snapshot_=std::move(snapshot);
157+
return result;
158+
}
159+
};
160+
#if defined(__linux__)
161+
using linux_shared_image_owner=basic_linux_shared_image_owner<linux_fd_ops>;
162+
#endif
163+
} // namespace webscene::graphics

0 commit comments

Comments
 (0)