diff --git a/src/Makefile b/src/Makefile index e5c94df..d8fa924 100644 --- a/src/Makefile +++ b/src/Makefile @@ -887,9 +887,27 @@ ifeq ($(lsx),yes) endif endif +### #8455 / #9170 - pthreads=no is the single-threaded, no-shared-memory build. +### +### WebKit, which is every browser on iOS including Chrome and Firefox for iOS, +### does not implement COEP: credentialless, so an ASCENDCHESS page is never +### cross-origin isolated there, SharedArrayBuffer is unavailable, and a pthread +### build cannot even be instantiated because it constructs a shared +### WebAssembly.Memory. Without -pthread there is no shared memory, no worker +### thread and no atomics requirement: SF_NO_THREADS runs a job inline on its +### caller, and SF_WASM_SINGLE replaces the stdin UCI loop with the sf_init / +### sf_command entry points JavaScript drives. The search then runs on the main +### WASM stack instead of a large-stack search thread, so that stack is 4MB. +### Upstream behavior is unchanged without pthreads=no. ifeq ($(arch),wasm32) - CXXFLAGS += -pthread -msimd128 -DUSE_POPCNT -DNNUE_EMBEDDING_OFF -DUSE_SLOPPY_ATOMICS - LDFLAGS += -pthread -sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=3MB + CXXFLAGS += -msimd128 -DUSE_POPCNT -DNNUE_EMBEDDING_OFF -DUSE_SLOPPY_ATOMICS + ifeq ($(pthreads),no) + CXXFLAGS += -DSF_NO_THREADS -DSF_WASM_SINGLE + LDFLAGS += -sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=4MB + else + CXXFLAGS += -pthread + LDFLAGS += -pthread -sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=3MB + endif ifeq ($(relaxedsimd),yes) CXXFLAGS += -mrelaxed-simd endif diff --git a/src/main.cpp b/src/main.cpp index 304b2be..a1815b1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,6 +28,63 @@ using namespace Stockfish; +#ifdef SF_WASM_SINGLE + + #include + +// #8455 / #9170 - entry points for the single-threaded WASM build. +// +// A browser has no usable blocking stdin, and this build has no worker thread +// to park a UCI reader on, so loop() is not used at all: JavaScript calls +// sf_init() once and then sf_command() per UCI line. Both run synchronously on +// whichever thread calls them, which is why the page loads this module inside a +// dedicated Web Worker (a plain Worker, NOT a pthread, so no shared memory is +// involved) and the search never blocks the UI. +// +// Engine output still reaches JavaScript the ordinary emscripten way, through +// Module.print / Module.printErr, so "info depth ..." lines stream out during +// the search rather than arriving in a batch at the end. + +namespace { +std::unique_ptr wasmEngine; +} + +extern "C" { + +EMSCRIPTEN_KEEPALIVE void sf_init() { + if (wasmEngine) + return; + + std::cout << engine_info() << std::endl; + + Attacks::init(); + Position::init(); + + // UCIEngine wants argv[0] to find its own directory. One argument means + // "no command line work to do", which is what loop() would have treated as + // interactive mode. The nets are written to the in-memory filesystem's + // working directory, which is where the engine looks first. + static char programName[] = "stockfish"; + static char* argv[] = {programName, nullptr}; + + wasmEngine = std::make_unique(CommandLine(1, argv)); + + Tune::init(wasmEngine->engine_options()); +} + +EMSCRIPTEN_KEEPALIVE void sf_command(const char* line) { + if (!wasmEngine || line == nullptr) + return; + + wasmEngine->execute(line); +} +} + +// Never run: the build is linked with -sINVOKE_RUN=0 and driven from sf_init. +int main() { return 0; } + +#else + #ifdef UNIVERSAL_BINARY namespace Stockfish { @@ -59,3 +116,5 @@ int main(int argc, char* argv[]) { int main(int argc, char* argv[]) { return Stockfish::main(argc, argv); } #endif #endif + +#endif // SF_WASM_SINGLE diff --git a/src/thread.cpp b/src/thread.cpp index d767268..e6cef84 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -55,15 +55,25 @@ Thread::Thread(Search::SharedState& sharedState, idx(n), idxInNuma(numaN), totalNuma(totalNumaCount), - nthreads(sharedState.options["Threads"]), + nthreads(sharedState.options["Threads"]) +#ifndef SF_NO_THREADS + , stdThread( - create_native_thread(NativeThreadOptions{}.setLargeStack(true), &Thread::idle_loop, this)) { - + create_native_thread(NativeThreadOptions{}.setLargeStack(true), &Thread::idle_loop, this)) +#endif +{ + +#ifdef SF_NO_THREADS + // #8455 / #9170 - there is no idle_loop to park, so nothing will ever clear + // this for us. + searching = false; +#else if (!stdThread.joinable()) { std::cerr << "Failed to create search thread\n"; std::exit(EXIT_FAILURE); } +#endif wait_for_search_finished(); @@ -87,8 +97,10 @@ Thread::~Thread() { assert(!searching); exit = true; +#ifndef SF_NO_THREADS start_searching(); stdThread.join(); +#endif } // Wakes up the thread that will start the search @@ -103,6 +115,17 @@ void Thread::clear_worker() { run_custom_job([this]() { worker->clear(); }); } +#ifdef SF_NO_THREADS + +// #8455 / #9170 - SF_NO_THREADS build: there is no worker thread, so a job runs +// inline on the caller and has already finished by the time control returns. +// Both functions keep their signatures so every call site compiles unchanged. +void Thread::wait_for_search_finished() {} + +void Thread::run_custom_job(std::function f) { f(); } + +#else + // Blocks on the condition variable until the thread has finished searching void Thread::wait_for_search_finished() { @@ -121,6 +144,8 @@ void Thread::run_custom_job(std::function f) { cv.notify_one(); } +#endif + void Thread::ensure_network_replicated() { worker->ensure_network_replicated(); } // Thread gets parked here, blocked on the condition variable diff --git a/src/thread.h b/src/thread.h index 111659c..d92c512 100644 --- a/src/thread.h +++ b/src/thread.h @@ -103,7 +103,9 @@ class Thread { std::condition_variable cv; usize idx, idxInNuma, totalNuma, nthreads; bool exit = false, searching = true; // Set before starting std::thread +#ifndef SF_NO_THREADS NativeThread stdThread; +#endif NumaReplicatedAccessToken numaAccessToken; }; diff --git a/src/uci.cpp b/src/uci.cpp index 5dd2d03..9d1cd07 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -104,88 +104,100 @@ void UCIEngine::loop() { && !getline(std::cin, cmd)) // Wait for an input or an end-of-file (EOF) indication cmd = "quit"; - currentCmd = cmd; - std::istringstream is(cmd); + token = execute(cmd); - token.clear(); // Avoid a stale if getline() returns nothing or a blank line - is >> token; + } while (token != "quit" && cli.argc <= 1); // The command-line arguments are one-shot +} - if (token == "quit" || token == "stop") - engine.stop(); +// #8455 / #9170 - the UCI dispatch, lifted verbatim out of loop() so that an +// embedded build with no stdin can feed commands in one at a time. Returns the +// command's first token, which is all loop() needed from the body. It also sets +// currentCmd, exactly as loop() did, so a critical error still names the command. +std::string UCIEngine::execute(const std::string& cmd) { + std::string token; - // The GUI sends 'ponderhit' to tell that the user has played the expected move. - // So, 'ponderhit' is sent if pondering was done on the same move that the user - // has played. The search should continue, but should also switch from pondering - // to the normal search. - else if (token == "ponderhit") - engine.set_ponderhit(false); + currentCmd = cmd; + std::istringstream is(cmd); - else if (token == "uci") - { - sync_cout << "id name " << engine_info(true) << "\n" - << engine.get_options() << sync_endl; + token.clear(); // Avoid a stale if getline() returns nothing or a blank line + is >> token; - sync_cout << "uciok" << sync_endl; - } + if (token == "quit" || token == "stop") + engine.stop(); - else if (token == "setoption") - setoption(is); - else if (token == "go") - { - // send info strings after the go command is sent for old GUIs and python-chess - print_info_string(engine.numa_config_information_as_string()); - print_info_string(engine.thread_allocation_information_as_string()); - go(is); - } - else if (token == "position") - position(is); - else if (token == "ucinewgame") - engine.search_clear(); - else if (token == "isready") - sync_cout << "readyok" << sync_endl; + // The GUI sends 'ponderhit' to tell that the user has played the expected move. + // So, 'ponderhit' is sent if pondering was done on the same move that the user + // has played. The search should continue, but should also switch from pondering + // to the normal search. + else if (token == "ponderhit") + engine.set_ponderhit(false); - // Add custom non-UCI commands, mainly for debugging purposes. - else if (token == "flip") - { - if (auto err = engine.flip()) - { - terminate_on_critical_error(err->what()); - } - } - else if (token == "bench") - bench(is); - else if (token == BenchmarkCommand) - benchmark(is); - else if (token == "d") - sync_cout << engine.visualize() << sync_endl; - else if (token == "eval") - engine.trace_eval(); - else if (token == "compiler") - sync_cout << compiler_info() << sync_endl; - else if (token == "export_net") - { - std::optional file; - std::string filename; + else if (token == "uci") + { + sync_cout << "id name " << engine_info(true) << "\n" + << engine.get_options() << sync_endl; - if (is >> filename) - file = path_from_utf8(filename); + sync_cout << "uciok" << sync_endl; + } - engine.save_network(file); + else if (token == "setoption") + setoption(is); + else if (token == "go") + { + // send info strings after the go command is sent for old GUIs and python-chess + print_info_string(engine.numa_config_information_as_string()); + print_info_string(engine.thread_allocation_information_as_string()); + go(is); + } + else if (token == "position") + position(is); + else if (token == "ucinewgame") + engine.search_clear(); + else if (token == "isready") + sync_cout << "readyok" << sync_endl; + + // Add custom non-UCI commands, mainly for debugging purposes. + else if (token == "flip") + { + if (auto err = engine.flip()) + { + terminate_on_critical_error(err->what()); } - else if (token == "--help" || token == "help" || token == "--license" || token == "license") - sync_cout - << "\nStockfish is a powerful chess engine for playing and analyzing." - "\nIt is released as free software licensed under the GNU GPLv3 License." - "\nStockfish is normally used with a graphical user interface (GUI) and implements" - "\nthe Universal Chess Interface (UCI) protocol to communicate with a GUI, an API, etc." - "\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme" - "\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n" - << sync_endl; - else if (!token.empty() && token[0] != '#') - sync_cout << "Unknown command: '" << cmd << "'. Type help for more information." - << sync_endl; + } + else if (token == "bench") + bench(is); + else if (token == BenchmarkCommand) + benchmark(is); + else if (token == "d") + sync_cout << engine.visualize() << sync_endl; + else if (token == "eval") + engine.trace_eval(); + else if (token == "compiler") + sync_cout << compiler_info() << sync_endl; + else if (token == "export_net") + { + std::optional file; + std::string filename; - } while (token != "quit" && cli.argc <= 1); // The command-line arguments are one-shot + if (is >> filename) + file = path_from_utf8(filename); + + engine.save_network(file); + } + else if (token == "--help" || token == "help" || token == "--license" || token == "license") + sync_cout + << "\nStockfish is a powerful chess engine for playing and analyzing." + "\nIt is released as free software licensed under the GNU GPLv3 License." + "\nStockfish is normally used with a graphical user interface (GUI) and implements" + "\nthe Universal Chess Interface (UCI) protocol to communicate with a GUI, an API, etc." + "\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme" + "\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n" + << sync_endl; + else if (!token.empty() && token[0] != '#') + sync_cout << "Unknown command: '" << cmd << "'. Type help for more information." + << sync_endl; + + return token; } Search::LimitsType UCIEngine::parse_limits(std::istream& is) { diff --git a/src/uci.h b/src/uci.h index 4d3c4cb..32d4ad2 100644 --- a/src/uci.h +++ b/src/uci.h @@ -43,6 +43,12 @@ class UCIEngine { void loop(); + // #8455 / #9170 - run ONE already-read UCI command and return its first + // token. loop() is now a thin reader over this, so the dispatch has exactly + // one definition. An embedded build (WASM, where there is no usable stdin) + // drives the engine through here instead of through loop(). + std::string execute(const std::string& cmd); + static int to_cp(Value v, const Position& pos); static std::string format_score(const Score& s); static std::string square(Square s);