i am currently trying to make a library which runs a wasm compiled cgal programm. the
the main.cpp looks like this:
#ifdef EMSCRIPTEN
// #define CGAL_ALWAYS_ROUND_TO_NEAREST
#define CGAL_NO_ASSERTIONS
#define CGAL_NO_PRECONDITIONS
#define CGAL_NO_POSTCONDITIONS
#define CGAL_NO_WARNINGS
#include <emscripten.h>
#include <emscripten/bind.h>
#endif
#include <iostream>
#include <vector>
#include <CGAL/Uncertain.h>
#include <CGAL/enum.h>
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Polygon_with_holes_2.h>
// #include <CGAL/create_weighted_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/create_weighted_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/create_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/Straight_skeleton_2/IO/print.h>
#include <boost/shared_ptr.hpp>
#include <cassert>
// Override make_certain to fix errors. Not sure why Uncertain doesn't work properly in Wasm environment.
template <>
bool CGAL::Uncertain<bool>::make_certain() const
{
return _i;
}
template <>
CGAL::Sign CGAL::Uncertain<CGAL::Sign>::make_certain() const
{
return _i;
}
typedef CGAL::Exact_predicates_inexact_constructions_kernel K;
typedef K::Point_2 Point;
typedef CGAL::Polygon_2<K> Polygon_2;
typedef CGAL::Polygon_with_holes_2<K> Polygon_with_holes;
typedef CGAL::Straight_skeleton_2<K> Ss;
typedef boost::shared_ptr<Ss> SsPtr;
typedef CGAL::Exact_predicates_inexact_constructions_kernel CGAL_KERNEL;
// Decodes rings from data and generates a weighted skeleton from them.
// Data contains a list of rings, each ring is represented by a number of points (uint32_t), followed by the points
// themselves (each point is represented by 2 floats: x, y).
// The last value is 0.
// Additionally, a vector of weights is provided, where each weight corresponds to a vertex.
SsPtr generate_weighted_skeleton(void *data, const std::vector<std::vector<float>> &weights)
{
uint32_t *data_uint32 = (uint32_t *)data;
uint32_t points = data_uint32[0];
assert(points != 0);
assert(points > 2);
++data_uint32;
Polygon_2 outer;
Polygon_2 hole;
Polygon_with_holes poly;
bool outer_set = false;
while (points != 0)
{
Polygon_2 *target = outer_set ? &hole : &outer;
for (long i = 0; i < points; i++)
{
float x = *((float *)data_uint32 + i * 2);
float y = *((float *)data_uint32 + i * 2 + 1);
target->push_back(Point(x, y));
}
data_uint32 += points * 2;
points = data_uint32[0];
++data_uint32;
if (!outer_set)
{
assert(outer.is_counterclockwise_oriented());
poly = Polygon_with_holes(outer);
outer_set = true;
}
else
{
assert(hole.is_clockwise_oriented());
poly.add_hole(hole);
hole.clear();
}
}
return CGAL::create_interior_weighted_straight_skeleton_2(poly, weights, CGAL_KERNEL());
}
// Serializes a skeleton into a format that can be sent to the JS side.
// The first part of the data describes the vertices:
// The first value (uint32_t) specifies the number of vertices.
// After that, each vertex is represented by 3 floats: x, y, time.
// Then, the second part describes the faces:
// Each face is represented by a uint32_t specifying the number of vertices in the face, followed by the indices
// of its vertices (also uint32_t).
// The last value is 0.
void *serialize_skeleton(SsPtr iss)
{
if (iss == nullptr)
{
return nullptr;
}
std::unordered_map<Ss::Vertex_const_handle, int> vertex_map;
std::vector<std::tuple<float, float, float>> vertices;
for (auto vertex = iss->vertices_begin(); vertex != iss->vertices_end(); ++vertex)
{
CGAL::Point_2 point = vertex->point();
vertices.emplace_back(point.x(), point.y(), vertex->time());
vertex_map[vertex] = vertices.size() - 1;
}
std::vector<std::vector<uint32_t>> faces;
int total_vertices = 0;
for (auto face = iss->faces_begin(); face != iss->faces_end(); ++face)
{
std::vector<uint32_t> face_polygon;
for (auto h = face->halfedge();;)
{
auto vertex_index = (uint32_t)vertex_map[h->vertex()];
face_polygon.push_back(vertex_index);
++total_vertices;
h = h->next();
if (h == face->halfedge())
{
break;
}
}
faces.emplace_back(face_polygon);
}
int total_size = 1 + vertices.size() * 3 + faces.size() + total_vertices + 1;
uint32_t *data = (uint32_t *)malloc(total_size * sizeof(uint32_t));
float *data_float = (float *)data;
int i = 0;
data[i++] = vertices.size();
for (auto vertex : vertices)
{
data_float[i++] = std::get<0>(vertex);
data_float[i++] = std::get<1>(vertex);
data_float[i++] = std::get<2>(vertex);
}
for (auto face : faces)
{
data[i++] = face.size();
for (auto vertex_index : face)
{
data[i++] = vertex_index;
}
}
data[i++] = 0;
return data;
}
extern "C"
{
EMSCRIPTEN_KEEPALIVE
void *create_weighted_straight_skeleton(void *data, const std::vector<std::vector<float>> &weights)
{
// // Polygon-Daten
// uint32_t polygon_data[] = {
// 4, // Anzahl der Punkte im äußeren Polygon
// 0, 0, // Punkt 1
// 10, 8, // Punkt 3
// 10, 10, // Punkt 3
// 0, 10, // Punkt 2
// // 4, // Anzahl der Punkte im inneren Loch
// // 2, 2, // Punkt 1
// // 2, 8, // Punkt 2
// // 8, 8, // Punkt 3
// // 8, 2, // Punkt 4
// 0 // Ende der Daten
// };
// // Gewichte
// std::vector<std::vector<float>> testweights = {
// {}, // Gewichte für das äußere Polygon
// // {0.5, 0.5, 0.5, 0.5} // Gewichte für das innere Loch
// };
SsPtr skeleton = generate_weighted_skeleton(data, weights);
return serialize_skeleton(skeleton);
}
}
the wapper in ts looks like this:
//@ts-ignore
import Module from '../core/build/main.js';
interface WasmModule {
HEAPU8: Uint8Array;
HEAPU32: Uint32Array;
HEAPF32: Float32Array;
_malloc(size: number): number;
_free(ptr: number): void;
_create_weighted_straight_skeleton(ptr: number, weightsPtr: number): number;
}
/**
* Each skeleton vertex is represented by x, y and time. Time can be used to calculate z coordinate.
*/
export type Vertex = [number, number, number];
/**
* Each polygon is represented by an array of vertex indices.
*/
export type Polygon = number[];
/**
* Straight skeleton calculation result.
*/
export interface Skeleton {
vertices: Vertex[];
polygons: Polygon[];
}
export class SkeletonBuilder {
private static module: WasmModule = null;
/**
* Initializes the WebAssembly module. Must be called before any other method.
*/
public static async init(): Promise<void> {
return Module().then((library: WasmModule) => {
this.module = library;
});
}
/**
* Builds a skeleton from a GeoJSON polygon.
* The polygon must have at least one ring. The first ring is always the outer ring, and the rest are inner rings.
* Outer rings must be counter-clockwise oriented and inner rings must be clockwise oriented.
* All rings must be weakly simple.
* Each ring must have a duplicate of the first vertex at the end.
* @param polygon The GeoJSON polygon.
* @param weights The weights for each vertex.
*/
public static buildFromGeoJSONPolygon(polygon: GeoJSON.Polygon, weights: number[][]): Skeleton {
this.checkModule();
return this.buildFromPolygon(polygon.coordinates, weights);
}
/**
* Builds a skeleton from a polygon represented as an array of rings.
* The polygon must have at least one ring. The first ring is always the outer ring, and the rest are inner rings.
* Outer rings must be counter-clockwise oriented and inner rings must be clockwise oriented.
* All rings must be weakly simple.
* Each ring must have a duplicate of the first vertex at the end.
* @param coordinates The polygon represented as an array of rings.
* @param weights The weights for each vertex.
*/
public static buildFromPolygon(coordinates: number[][][], weights: number[][]): Skeleton {
this.checkModule();
const inputBuffer = this.serializeInput(coordinates);
const inputPtr = this.module._malloc(inputBuffer.byteLength);
this.module.HEAPU8.set(new Uint8Array(inputBuffer), inputPtr);
const weightsBuffer = this.serializeWeights(weights);
console.log("DEBUG WEIGHTSBUFFER", new Float32Array(weightsBuffer))
const weightsPtr = this.module._malloc(weightsBuffer.byteLength);
this.module.HEAPU8.set(new Uint8Array(weightsBuffer), weightsPtr);
const ptr = this.module._create_weighted_straight_skeleton(inputPtr, weightsPtr);
if (ptr === 0) {
return null;
}
let offset = ptr / 4;
const arrayU32 = this.module.HEAPU32;
const arrayF32 = this.module.HEAPF32;
const vertices: Vertex[] = [];
const polygons: number[][] = [];
const vertexCount = arrayU32[offset++];
for (let i = 0; i < vertexCount; i++) {
const x = arrayF32[offset++];
const y = arrayF32[offset++];
const time = arrayF32[offset++];
vertices.push([x, y, time]);
}
let polygonVertexCount = arrayU32[offset++];
while (polygonVertexCount > 0) {
const polygon = [];
for (let i = 0; i < polygonVertexCount; i++) {
polygon.push(arrayU32[offset++]);
}
polygons.push(polygon);
polygonVertexCount = arrayU32[offset++];
}
// this.module._free(ptr);
// this.module._free(inputPtr);
// this.module._free(weightsPtr);
return { vertices, polygons };
}
private static checkModule(): void {
if (this.module === null) {
throw new Error('The WebAssembly module has not been initialized, call SkeletonBuilder.init() first.');
}
}
private static serializeInput(input: number[][][]): ArrayBuffer {
let size: number = 1;
for (const ring of input) {
size += 1 + (ring.length - 1) * 2;
}
const uint32Array = new Uint32Array(size);
const float32Array = new Float32Array(uint32Array.buffer);
let offset = 0;
for (const ring of input) {
uint32Array[offset++] = ring.length - 1;
for (let i = 0; i < ring.length - 1; i++) {
float32Array[offset++] = ring[i][0];
float32Array[offset++] = ring[i][1];
}
}
uint32Array[offset++] = 0;
return float32Array.buffer;
}
private static serializeWeights(weights: number[][]): ArrayBuffer {
let size: number = 0;
for (const ringWeights of weights) {
size += ringWeights.length;
}
const float32Array = new Float32Array(size);
let offset = 0;
for (const ringWeights of weights) {
for (const weight of ringWeights) {
float32Array[offset++] = weight;
}
}
return float32Array.buffer;
}
}
the issue seems to be the serializeWeights function. somehow i am messing up the types or pointers, or anything i am not seeing.
once i am using weights i get a 'memory access out of bounds' error from wasm. if i do not use the weights which were transfered to the cpp but instead use an empty {} here in the last line of the cpp (generate_weighted_skeleton(data, testWeights); then the code runs and does not return issues.
my guess is that i am setting a wrong buffer here:
const weightsBuffer = this.serializeWeights(weights);
console.log("DEBUG WEIGHTSBUFFER", new Float32Array(weightsBuffer))
const weightsPtr = this.module._malloc(weightsBuffer.byteLength);
this.module.HEAPU8.set(new Uint8Array(weightsBuffer), weightsPtr);
but i cannot figure out what the issue is here.
since i have no idea how i would log something from the cpp file once it is compiled for wasm, i am pretty stuck to figure out what the problem is.
if anyone have any idea how i can proceed to find the problem i would be verry happy!!
The answer has not to do with wasm, but more the datatype. on the typescript side i had to change the way the buffer was made:
on the cpp side the it also needed to be adapted to the type:
cheers. its working now :)