nissy-core

The "engine" of nissy, including the H48 optimal solver.
git clone https://git.tronto.net/nissy-core
Download | Log | Files | Refs | README | LICENSE

commit ecb04c6b7fbf5d06c2fdce5128e83575cac2bd21
parent c6a77f30f64be73a5e55e06336975f2ecfbb2324
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date:   Fri, 23 May 2025 17:06:02 +0200

Web version (work in progress)

Diffstat:
M.gitignore | 2++
MREADME.md | 13+++++++++++++
Mbuild | 34++++++++++++++++++++++------------
Mweb/adapter.cpp | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mweb/callback.js | 32++++++++++++++++++++++++++++----
Mweb/examples/solve.mjs | 12+++++++-----
Aweb/http/index.html | 20++++++++++++++++++++
Aweb/http/mime | 1+
Aweb/storage.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aweb/storage.h | 7+++++++
10 files changed, 247 insertions(+), 32 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -22,6 +22,8 @@ runcpp runtest.js runtest.wasm web/*.wasm +web/http/*.wasm +web/http/*.mjs web/nissy_web_module.* runtool tools/.DS_Store diff --git a/README.md b/README.md @@ -251,6 +251,19 @@ using [nodejs](https://nodejs.org): $ node web/examples/[filename] ``` +An example web app running nissy can be found in the `web/http` folder. +You can run a web server in that folder to check it out, but you need +to set some extra headers to make it work. For example, if you are using +[darkhttpd](https://github.com/emikulic/darkhttpd) you can start the server +with the following command: + +``` +$ darkhttpd web/http/ \ + --header 'Cross-Origin-Opener-Policy: same-origin' \ + --header 'Cross-Origin-Embedder-Policy: require-corp' \ + --mimetypes web/http/mime +``` + ## Cube format This format is a "base 32" encoding of the cube. It is not meant to be diff --git a/build b/build @@ -129,9 +129,12 @@ CPPFLAGS="-std=c++20 -pthread" # Build flags for emscripten (WASM target) WASMCFLAGS="-std=c11 -fPIC -D_POSIX_C_SOURCE=199309L -pthread" WASMMFLAGS="-DTHREADS=$THREADS -DWASMSIMD" +#WASMLINKFLAGS="--no-entry -sEXPORT_NAME='Nissy' -sMODULARIZE \ +# -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=5MB -sPTHREAD_POOL_SIZE=$THREADS \ +# -sNODERAWFS -sASYNCIFY -sLINKABLE -sEXPORT_ALL" WASMLINKFLAGS="--no-entry -sEXPORT_NAME='Nissy' -sMODULARIZE \ -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=5MB -sPTHREAD_POOL_SIZE=$THREADS \ - -sLINKABLE -sEXPORT_ALL" + -sASYNCIFY -sLINKABLE -sEXPORT_ALL" if (command -v "python3-config" >/dev/null 2>&1) ; then PYTHON3_INCLUDES="$(python3-config --includes)" @@ -207,7 +210,7 @@ odflags() { build_clean() { run rm -rf -- *.o *.so *.a run runtest runtool runcpp \ - web/nissy_web_module.* + web/nissy_web_module.* web/*.wasm web/http/*.mjs web/http/*.wasm } build_nissy() { @@ -233,7 +236,7 @@ build_sharedlib() { } build_shell() { - build_nissy + build_nissy || exit 1 run $CC $CFLAGS $WFLAGS $(odflags) -o run nissy.o shell/shell.c } @@ -243,7 +246,7 @@ build_python() { echo "Cannot build python module" exit 1 fi - build_nissy + build_nissy || exit 1 run $CC $CFLAGS $WFLAGS $PYTHON3_INCLUDES $(odflags) -shared \ -o nissy_python_module.so nissy.o python/nissy_module.c } @@ -255,7 +258,7 @@ build_cpp() { echo "usage: ./build cpp FILES" fi - build_nissy + build_nissy || exit 1 run $CXX -std=c++20 -o runcpp cpp/nissy.cpp nissy.o $@ run ./runcpp } @@ -265,13 +268,20 @@ build_web() { validate_threads "$THREADS" validate_arch "$ARCH" + obj="nissy_web_module" + run $EMCC $WASMCFLAGS $WFLAGS $WASMMFLAGS $(odflags) -c \ - -o nissy.o src/nissy.c - run $EMCC -lembind -lidbfs.js -lnodefs.js \ + -o nissy.o src/nissy.c || exit 1 + #run $EMCC -lembind -lidbfs.js -lnodefs.js -lnoderawfs.js \ + # $CPPFLAGS $(odflags) $WASMLINKFLAGS \ + # --js-library web/callback.js -o web/"$obj".mjs \ + # cpp/nissy.cpp web/storage.cpp web/adapter.cpp nissy.o + run $EMCC -lembind -lidbfs.js \ $CPPFLAGS $(odflags) $WASMLINKFLAGS \ - --js-library web/callback.js \ - -o web/nissy_web_module.mjs \ - cpp/nissy.cpp web/adapter.cpp nissy.o + --js-library web/callback.js -o web/"$obj".mjs \ + cpp/nissy.cpp web/storage.cpp web/adapter.cpp nissy.o || exit 1 + cp web/"$obj".mjs web/http + cp web/"$obj".wasm web/http } dotest() { @@ -310,7 +320,7 @@ build_test_generic() { shift fi debug="yes" - build_$obj + build_$obj || exit 1 for t in test/*; do dotest || exit 1 done @@ -323,7 +333,7 @@ build_test() { testbuild="$CC $CFLAGS $WFLAGS $DFLAGS $MFLAGS -o $testobj" testrun="./$testobj" build_test_generic $@ - rm runtest + rm -f runtest } build_webtest() { diff --git a/web/adapter.cpp b/web/adapter.cpp @@ -1,11 +1,15 @@ #include "../cpp/nissy.h" +#include "storage.h" +#include <emscripten.h> #include <emscripten/bind.h> #include <map> #include <set> #include <string> #include <vector> +EM_ASYNC_JS(void, fake_async, (), {}); + extern "C" { extern int addCallbackFunction(/* args intentionally unspecified */); extern void callFunction(int, const char *); @@ -47,26 +51,43 @@ const std::set<std::string> available_solvers std::map<std::string, nissy::solver> loaded_solvers; // TODO: this should ask the user if they want to download or generate. -// TODO: this should also save the data to a file (IDBFS / NODEFS) bool init_solver(const std::string& name) { auto se = nissy::solver::get(name); nissy::solver solver = std::get<nissy::solver>(se); - log("Generating data for solver " + solver.name + "\n"); - auto err = solver.generate_data(); - if (!err.ok()) { - log("Error generating the data!\n"); - return false; + + solver.data.resize(solver.size); + if (storage::read(solver.id, solver.size, + reinterpret_cast<char *>(solver.data.data()))) { + log("Data for solver " + solver.name + " read from storage\n"); + } else { + log("Could not read data for solver " + solver.name + + " from storage, generating it\n"); + auto err = solver.generate_data(); + + if (!err.ok()) { + log("Error generating the data!\n"); + return false; + } } + log("Checking data integrity " "(this is done only once per session per solver)...\n"); if (!solver.check_data().ok()) { - log("Error generating the data!\n"); + log("Error! Data is corrupted!\n"); return false; } loaded_solvers.insert({name, solver}); - log("Data generated successfully, but not saved " - "(feature not yet available)\n"); + + if (storage::write(solver.id, solver.size, + reinterpret_cast<const char *>(solver.data.data()))) { + log("Data for solver " + solver.name + " stored\n"); + } else { + log("Error storing the data (the solver is usable, " + "but the data will have to be re-generated next " + "time you want to use it)"); + } + return true; } @@ -87,17 +108,31 @@ bool solver_valid(const std::string& name) int poll_status(void *arg) { + return nissy::status::RUN.value; +/* +TODO: reintroduce poll status int id = *(int *)arg; if (id == -1) return nissy::status::RUN.value; return callFunctionInt(id); +*/ } +#if 0 + nissy::solver::solve_result solve(std::string name, nissy::cube cube, nissy::nissflag nissflag, unsigned minmoves, unsigned maxmoves, unsigned maxsols, unsigned optimal, unsigned threads, int poll_status_id) { + // Here we use a dirty trick to make this function always return the + // same kind of JavaScript object. If we did not do this, the returned + // object would be a Promise on the first run of the solver for each + // session (because when loading the table some async JS code is + // called), and a regular object otherwise. + // TODO figure out if there is a better way to do this. + fake_async(); + if (!solver_valid(name)) return nissy::solver::solve_result {.err = nissy::error::INVALID_SOLVER}; @@ -106,6 +141,35 @@ nissy::solver::solve_result solve(std::string name, maxmoves, maxsols, optimal, threads, NULL, &poll_status_id); } +#else + +std::string solve(std::string name, + nissy::cube cube, nissy::nissflag nissflag, unsigned minmoves, + unsigned maxmoves, unsigned maxsols, unsigned optimal, unsigned threads, + int poll_status_id) +{ + // Here we use a dirty trick to make this function always return the + // same kind of JavaScript object. If we did not do this, the returned + // object would be a Promise on the first run of the solver for each + // session (because when loading the table some async JS code is + // called), and a regular object otherwise. + // TODO figure out if there is a better way to do this. + fake_async(); + + if (!solver_valid(name)) + return ""; +/* + return nissy::solver::solve_result + {.err = nissy::error::INVALID_SOLVER}; +*/ + + return loaded_solvers.at(name).solve(cube, nissflag, minmoves, + maxmoves, maxsols, optimal, threads, NULL, &poll_status_id) + .solutions[0]; +} + +#endif + EMSCRIPTEN_BINDINGS(Nissy) { emscripten::class_<nissy::nissflag>("NissFlag") @@ -134,13 +198,11 @@ EMSCRIPTEN_BINDINGS(Nissy) emscripten::constant("statusRUN", nissy::status::RUN.value); emscripten::constant("statusSTOP", nissy::status::STOP.value); emscripten::constant("statusPAUSE", nissy::status::PAUSE.value); - /* emscripten::class_<nissy::status>("Status") .class_property("run", &nissy::status::RUN) .class_property("stop", &nissy::status::STOP) .class_property("pause", &nissy::status::PAUSE) ; - */ emscripten::class_<nissy::cube>("Cube") .constructor<>() @@ -150,10 +212,17 @@ EMSCRIPTEN_BINDINGS(Nissy) .function("toString", &nissy::cube::to_string) ; + emscripten::register_vector<std::string>("StringVector"); + emscripten::value_array<nissy::solver::solve_result>("SolveResult") + .element(&nissy::solver::solve_result::err) + .element(&nissy::solver::solve_result::solutions) + ; +/* emscripten::class_<nissy::solver::solve_result>("SolveResult") .property("err", &nissy::solver::solve_result::err) .property("solutions", &nissy::solver::solve_result::solutions) ; +*/ emscripten::function("countMoves", &nissy::count_moves); emscripten::function("solve", &solve, diff --git a/web/callback.js b/web/callback.js @@ -2,20 +2,44 @@ addToLibrary({ cbfl: [], +validateCallbackId__deps: [ 'cbfl' ], +validateCallbackId: function(i) { + if (i < 0) { + console.log("--- WARNING ---"); + console.log("Trying to access callback function of invalid id " + i); + console.log("--- WARNING ---"); + return false; + } + + if (i >= _cbfl.length) { + console.log("--- WARNING ---"); + console.log("Trying to access callback function " + i + ", but only " + + _cbfl.length + " have been registered. This may be caused by a " + + "call outside of the main thread."); + console.log("--- WARNING ---"); + return false; + } + + return true; +}, + addCallbackFunction__deps: [ 'cbfl' ], addCallbackFunction: function(f) { _cbfl.push(f) return _cbfl.length - 1 }, -callFunction__deps: [ 'cbfl' ], +callFunction__deps: [ 'cbfl', 'validateCallbackId' ], callFunction: function(id, arg) { - _cbfl[id](UTF8ToString(arg)) + if (_validateCallbackId(id)) + _cbfl[id](UTF8ToString(arg)); }, -callFunctionInt__deps: [ 'cbfl' ], +callFunctionInt__deps: [ 'cbfl', 'validateCallbackId' ], callFunctionInt: function(id) { - return _cbfl[id]() + if (_validateCallbackId(id)) + return _cbfl[id](); + return 0; }, }); diff --git a/web/examples/solve.mjs b/web/examples/solve.mjs @@ -1,10 +1,12 @@ import Nissy from '../nissy_web_module.mjs' -const nissy = await Nissy() +const nissy = await Nissy(); -nissy.setLogger(nissy._addCallbackFunction(console.log)) +var log = process.stdout.write.bind(process.stdout); +//var log = console.log +nissy.setLogger(nissy._addCallbackFunction(log)) -var cube = new nissy.Cube() -cube.move('R\' U\' F') +var cube = new nissy.Cube(); +cube.move('R\' U\' F'); -nissy.solve('h48h0k4', cube, nissy.NissFlag.normal, 0, 8, 2, 99, 4, -1) +nissy.solve('h48h0k4', cube, nissy.NissFlag.normal, 0, 8, 2, 99, 4, -1); diff --git a/web/http/index.html b/web/http/index.html @@ -0,0 +1,20 @@ +<!doctype html> +<html lang="en-US"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width" /> + <title>Nissy - H48 solver POC</title> + <script type="module" src="./solve.mjs"></script> + </head> + <body> + <input id="scrambleText" placeholder="Type the scramble here..."> + <select id="solverSelector"> + <option value="h48h0k4" selected="selected">h48 h=0 k=4 (59Mb)</option> + <option value="h48h3k2">h48 h=3 k=2 (283Mb)</option> + <option value="h48h7k2">h48 h=7 k=2 (3.6Gb)</option> + </select> + <button id="solveButton">Solve!</button> + <p>Solution:</p> + <p id="solution"></p> + </body> +</html> diff --git a/web/http/mime b/web/http/mime @@ -0,0 +1 @@ +text/javascript mjs diff --git a/web/storage.cpp b/web/storage.cpp @@ -0,0 +1,67 @@ +#include "storage.h" + +#include "emscripten.h" +#include <filesystem> +#include <fstream> + +EM_JS(int, inbrowser, (), { return typeof window !== 'undefined'; }); +EM_JS(int, inworker, (), { return typeof WorkerGlobalScope !== 'undefined' && + self instanceof WorkerGlobalScope; }); + +std::string getprefix() { + return inbrowser() || inworker() ? "/tables/" : "./tables/"; +} + +EM_ASYNC_JS(int, loadfs, (), { + const dir = '/tables'; + const inBrowser = typeof window !== 'undefined'; + const inWorker = typeof WorkerGlobalScope !== 'undefined' && + self instanceof WorkerGlobalScope; + + if (!(inBrowser || inWorker)) return; + + if (!FS.analyzePath(dir).exists) + FS.mkdir(dir); + + if (FS.analyzePath(dir).object.mount.mountpoint != dir) { + FS.mount(IDBFS, { autoPersist: true }, dir); + + await new Promise((resolve, reject) => { + FS.syncfs(true, function (err) { + if (err) { + reject(err); + } else { + resolve(true); + } + }); + }); + } +}); + +bool storage::read(std::string key, size_t data_size, char *data) +{ + loadfs(); + + std::filesystem::path path(getprefix() + key); + if (!std::filesystem::exists(path)) + return false; + + std::ifstream ifs(path, std::ios::binary); + ifs.read(data, data_size); + ifs.close(); + + return !ifs.fail(); +} + +bool storage::write(std::string key, size_t data_size, const char *data) +{ + loadfs(); + + std::filesystem::path path(getprefix() + key); + + std::ofstream ofs(path, std::ios::binary); + ofs.write(data, data_size); + ofs.close(); + + return !ofs.fail(); +} diff --git a/web/storage.h b/web/storage.h @@ -0,0 +1,7 @@ +#include <string> +#include <fstream> + +namespace storage { + bool read(std::string, size_t, char *); + bool write(std::string, size_t, const char *); +}