chore: checkpoint web Blender migration source, tools and docs
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
mes123456
2026-11-10 11:18:01 +08:00
parent 122142c26c
commit f3e21c6b98
6887 changed files with 257154 additions and 12294 deletions

View File

@@ -18,6 +18,7 @@ set(SRC
gpu_py_buffer.cc
gpu_py_capabilities.cc
gpu_py_compute.cc
gpu_py_compute_web.cc
gpu_py_element.cc
gpu_py_framebuffer.cc
gpu_py_matrix.cc

View File

@@ -116,6 +116,23 @@ static PyObject *pygpu_compute_dispatch(PyObject * /*self*/, PyObject *args, PyO
#endif
static PyMethodDef pygpu_compute__tp_methods[] = {
#ifdef __EMSCRIPTEN__
{"dispatch_glsl",
reinterpret_cast<PyCFunction>(bpygpu_compute_dispatch_glsl),
METH_VARARGS | METH_KEYWORDS,
"dispatch_glsl(source, buffers, workgroups, entry_point='main', binding_types=None, "
"dispatches=None, timeout_ms=30000) -> tuple[bytes, ...]. "
"Compile GLSL compute to WGSL with native Naga WASM, execute on WebGPU, "
"and return completed storage contents. Use explicit set=0 binding indices."},
{"dispatch_wgsl",
reinterpret_cast<PyCFunction>(bpygpu_compute_dispatch_wgsl),
METH_VARARGS | METH_KEYWORDS,
"dispatch_wgsl(source, buffers, workgroups, entry_point='main', binding_types=None, "
"dispatches=None, timeout_ms=30000) -> tuple[bytes, ...]. "
"Execute real WebGPU buffer compute and return completed buffer contents. "
"Bindings use group 0 and their sequence index. Inputs are bytes-like or output byte sizes. "
"Requires an isolated browser engine worker; never falls back to CPU kernel execution."},
#endif
{"dispatch",
reinterpret_cast<PyCFunction>(pygpu_compute_dispatch),
METH_VARARGS | METH_KEYWORDS,

View File

@@ -13,5 +13,9 @@
namespace blender {
[[nodiscard]] PyObject *bpygpu_compute_init();
#ifdef __EMSCRIPTEN__
PyObject *bpygpu_compute_dispatch_wgsl(PyObject *self, PyObject *args, PyObject *kwargs);
PyObject *bpygpu_compute_dispatch_glsl(PyObject *self, PyObject *args, PyObject *kwargs);
#endif
} // namespace blender

View File

@@ -0,0 +1,160 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
* SPDX-License-Identifier: GPL-2.0-or-later */
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#ifdef __EMSCRIPTEN__
# include <emscripten.h>
# include <cstdint>
# include <memory>
# include <vector>
# include <exception>
/* This is a browser API transport, not a CPU implementation of the kernel.
* The owning engine worker blocks while a separate worker submits actual
* WebGPU compute commands and maps their completed storage buffers. */
EM_JS(int, bpy_webgpu_compute, (const char *metadata, const uint32_t *buffers, int count,
int timeout, char *error, int error_capacity), {
try {
if (typeof Module['webGPUComputeSync'] !== 'function') {
throw new Error('This WebEngine host has no native WebGPU compute transport');
}
Module['webGPUComputeSync'](metadata, buffers, count, timeout);
return 0;
} catch (reason) {
stringToUTF8(String(reason && reason.message || reason), error, error_capacity);
return -1;
}
});
namespace blender {
namespace {
struct PyRelease { void operator()(PyObject *value) const { Py_XDECREF(value); } };
using PyOwned = std::unique_ptr<PyObject, PyRelease>;
constexpr Py_ssize_t max_buffer_bytes = 64 * 1024 * 1024;
constexpr Py_ssize_t max_total_bytes = 256 * 1024 * 1024;
}
static PyObject *compute_dispatch_source(PyObject *args, PyObject *kwargs, const char *language)
{
const char *source;
Py_ssize_t source_length;
PyObject *buffers_arg, *groups, *kinds_arg = Py_None, *steps = Py_None;
const char *entry_point = "main";
int timeout = 30000;
static const char *keywords[] = {"source", "buffers", "workgroups", "entry_point",
"binding_types", "dispatches", "timeout_ms", nullptr};
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s#OO|sOOi:dispatch_wgsl",
const_cast<char **>(keywords), &source, &source_length,
&buffers_arg, &groups, &entry_point, &kinds_arg, &steps, &timeout))
{
return nullptr;
}
if (source_length <= 0 || source_length > 1024 * 1024 || timeout < 100 || timeout > 120000) {
PyErr_SetString(PyExc_ValueError, "Compute source or timeout exceeds the native request limit");
return nullptr;
}
try {
PyOwned buffers(PySequence_Fast(buffers_arg, "buffers must be a sequence of buffer objects or byte sizes"));
if (!buffers) { return nullptr; }
const Py_ssize_t count = PySequence_Fast_GET_SIZE(buffers.get());
if (count < 1 || count > 16) {
PyErr_SetString(PyExc_ValueError, "Compute requires between 1 and 16 buffers");
return nullptr;
}
PyOwned kinds;
if (kinds_arg == Py_None) {
kinds.reset(PyList_New(count));
if (!kinds) { return nullptr; }
for (Py_ssize_t i = 0; i < count; i++) {
PyObject *kind = PyUnicode_FromString("storage");
if (!kind) { return nullptr; }
PyList_SET_ITEM(kinds.get(), i, kind);
}
}
else {
kinds.reset(PySequence_List(kinds_arg));
if (!kinds) { return nullptr; }
if (PyList_GET_SIZE(kinds.get()) != count) {
PyErr_SetString(PyExc_ValueError, "binding_types must match buffers");
return nullptr;
}
}
PyOwned metadata(Py_BuildValue("{s:s,s:s#,s:s,s:O,s:O,s:O}", "language", language, "source", source, source_length,
"entryPoint", entry_point, "groups", groups,
"kinds", kinds.get(), "steps", steps));
if (!metadata) { return nullptr; }
PyOwned json_module(PyImport_ImportModule("json"));
if (!json_module) { return nullptr; }
PyOwned encoded(PyObject_CallMethod(json_module.get(), "dumps", "O", metadata.get()));
if (!encoded) { return nullptr; }
const char *json = PyUnicode_AsUTF8(encoded.get());
if (!json) { return nullptr; }
std::vector<PyOwned> copies;
std::vector<uint32_t> descriptors;
Py_ssize_t total = 0;
for (Py_ssize_t i = 0; i < count; i++) {
PyObject *item = PySequence_Fast_GET_ITEM(buffers.get(), i);
PyOwned bytes;
if (PyLong_Check(item) && !PyBool_Check(item)) {
const Py_ssize_t size = PyLong_AsSsize_t(item);
if (size == -1 && PyErr_Occurred()) { return nullptr; }
if (size < 4 || size % 4 || size > max_buffer_bytes || size > max_total_bytes - total) {
PyErr_SetString(PyExc_ValueError, "Compute buffer size must be aligned and within the native budget");
return nullptr;
}
// Python bytearray(size) defines real zero-initialized output storage.
bytes.reset(PyObject_CallFunction(reinterpret_cast<PyObject *>(&PyByteArray_Type), "n", size));
}
else {
Py_buffer view;
if (PyObject_GetBuffer(item, &view, PyBUF_CONTIG_RO) < 0) { return nullptr; }
if (view.len < 4 || view.len % 4 || view.len > max_buffer_bytes || view.len > max_total_bytes - total) {
PyBuffer_Release(&view);
PyErr_SetString(PyExc_ValueError, "Compute input must be contiguous, aligned and within the native budget");
return nullptr;
}
bytes.reset(PyByteArray_FromStringAndSize(static_cast<const char *>(view.buf), view.len));
PyBuffer_Release(&view);
}
if (!bytes) { return nullptr; }
const Py_ssize_t size = PyByteArray_GET_SIZE(bytes.get());
total += size;
descriptors.push_back(uint32_t(reinterpret_cast<uintptr_t>(PyByteArray_AS_STRING(bytes.get()))));
descriptors.push_back(uint32_t(size));
copies.push_back(std::move(bytes));
}
char error[4096] = {};
if (bpy_webgpu_compute(json, descriptors.data(), int(count), timeout, error, sizeof(error)) != 0) {
PyErr_SetString(PyExc_RuntimeError, error);
return nullptr;
}
PyOwned results(PyTuple_New(count));
if (!results) { return nullptr; }
for (Py_ssize_t i = 0; i < count; i++) {
PyObject *value = PyBytes_FromStringAndSize(PyByteArray_AS_STRING(copies[i].get()),
PyByteArray_GET_SIZE(copies[i].get()));
if (!value) { return nullptr; }
PyTuple_SET_ITEM(results.get(), i, value);
}
return results.release();
}
catch (const std::bad_alloc &) { return PyErr_NoMemory(); }
catch (const std::exception &error) {
PyErr_SetString(PyExc_RuntimeError, error.what());
return nullptr;
}
}
PyObject *bpygpu_compute_dispatch_wgsl(PyObject *, PyObject *args, PyObject *kwargs)
{
return compute_dispatch_source(args, kwargs, "wgsl");
}
PyObject *bpygpu_compute_dispatch_glsl(PyObject *, PyObject *args, PyObject *kwargs)
{
return compute_dispatch_source(args, kwargs, "glsl");
}
} // namespace blender
#endif

View File

@@ -685,10 +685,13 @@ static PyObject *pygpu_texture_read(BPyGPUTexture *self)
}
void *buf = GPU_texture_read(self->tex, best_data_format, 0);
const Py_ssize_t component_len = GPU_texture_component_len(tex_format);
const Py_ssize_t component_len = best_data_format == GPU_DATA_10_11_11_REV ?
1 : GPU_texture_component_len(tex_format);
Py_ssize_t shape[4];
int shape_len;
if (GPU_texture_dimensions(self->tex) == 3) {
if (GPU_texture_dimensions(self->tex) == 3 ||
(GPU_texture_dimensions(self->tex) == 2 && GPU_texture_is_array(self->tex)))
{
shape[0] = GPU_texture_depth(self->tex);
shape[1] = GPU_texture_height(self->tex);
shape[2] = GPU_texture_width(self->tex);