chore: checkpoint web Blender migration source, tools and docs
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
160
blender-5.2.0/source/blender/python/gpu/gpu_py_compute_web.cc
Normal file
160
blender-5.2.0/source/blender/python/gpu/gpu_py_compute_web.cc
Normal 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
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user