Skip to content

Commit

Permalink
Further fixes and improvements related to WebAssembly (#9444)
Browse files Browse the repository at this point in the history
  • Loading branch information
oleg-derevenetz authored Jan 15, 2025
1 parent 6f55a58 commit 2498a89
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 46 deletions.
1 change: 1 addition & 0 deletions .github/workflows/make.yml
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ jobs:
emmake make -f Makefile.emscripten -j "$(nproc)"
env:
FHEROES2_STRICT_COMPILATION: ON
FHEROES2_WITH_THREADS: ON
- name: Create package
run: |
# Translations and H2D files are already included in fheroes2.data
Expand Down
1 change: 1 addition & 0 deletions Makefile.emscripten
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#
# FHEROES2_STRICT_COMPILATION: build in strict compilation mode (turns warnings into errors)
# FHEROES2_WITH_DEBUG: build in debug mode
# FHEROES2_WITH_THREADS: build with multithreading support
# FHEROES2_DATA: set the built-in path to the fheroes2 data directory (e.g. /usr/share/fheroes2)

.PHONY: all clean translations
Expand Down
6 changes: 3 additions & 3 deletions docs/README_emscripten.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ emmake make -f Makefile.emscripten

### Building with additional parameters

If you want to specify some additional ad hoc build parameters, you can use the appropriate environment variables for this, for example:
If you want to specify some additional ad hoc build parameters, you can use the appropriate environment variables for this. For example, the following command:

```sh
FHEROES2_WITH_DEBUG=ON LDFLAGS="-sMODULARIZE -sEXPORTED_RUNTIME_METHODS=run -sEXPORT_NAME=fheroes2" emmake make -f Makefile.emscripten
FHEROES2_WITH_THREADS=ON LDFLAGS="-sMODULARIZE -sEXPORTED_RUNTIME_METHODS=run -sEXPORT_NAME=fheroes2" emmake make -f Makefile.emscripten
```

will build fheroes2 in debug mode with additional parameters to create a module.
will build a WebAssembly binary with multithreading support, as well as with additional parameters for creating a module.

## Running the Wasm port on a web server

Expand Down
14 changes: 6 additions & 8 deletions src/dist/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #
###########################################################################

#
# Name of the main target executable file
#

TARGET := fheroes2

#
# Common build options for fheroes2 and third-party libraries
#
Expand Down Expand Up @@ -80,14 +86,6 @@ endif

include Makefile.$(PLATFORM)

#
# Name of the main target executable file
# This name can be platform-specific (and can already be defined in a platform-specific Makefile)
#
ifeq ($(origin TARGET),undefined)
TARGET := fheroes2
endif

#
# Build options for third-party libraries
#
Expand Down
31 changes: 26 additions & 5 deletions src/dist/Makefile.emscripten
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,45 @@ CCFLAGS := $(CCFLAGS) \
--use-port=sdl2 \
--use-port=sdl2_mixer \
--use-port=zlib \
-sSDL2_MIXER_FORMATS=$(SDL2_MIXER_FORMATS)
-sSDL2_MIXER_FORMATS=$(SDL2_MIXER_FORMATS) \
-sSTRICT

LDFLAGS := $(LDFLAGS) \
--preload-file ../../../files/data/resurrection.h2d@/files/data/resurrection.h2d \
--preload-file ../../../files/lang/@/files/lang/ \
--preload-file ../../../files/timidity/@/etc/ \
-sALLOW_MEMORY_GROWTH \
-sASYNCIFY \
-sASYNCIFY_STACK_SIZE=32768 \
-sENVIRONMENT=web,worker \
-sASYNCIFY_STACK_SIZE=65536 \
-sINCOMING_MODULE_JS_API=canvas,preRun,setStatus \
-sINITIAL_MEMORY=128mb \
-sMEMORY_GROWTH_LINEAR_STEP=32mb \
-sNO_DISABLE_EXCEPTION_CATCHING \
-sPTHREAD_POOL_SIZE=8 \
-sSDL2_MIXER_FORMATS=$(SDL2_MIXER_FORMATS) \
-sSTACK_SIZE=256kb \
-sSTRICT \
-lGL \
-lhtml5 \
-lidbfs.js

ifdef FHEROES2_WITH_DEBUG
CCFLAGS := $(CCFLAGS) -gsource-map
LDFLAGS := $(LDFLAGS) -gsource-map
LDFLAGS := $(LDFLAGS) -O0 -gsource-map
else
CCFLAGS := $(CCFLAGS) -flto
LDFLAGS := $(LDFLAGS) -O3 -flto
endif

ifdef FHEROES2_WITH_THREADS
LDFLAGS := $(LDFLAGS) \
-Wno-pthreads-mem-growth \
-sALLOW_BLOCKING_ON_MAIN_THREAD \
-sENVIRONMENT=web,worker \
-sPTHREAD_POOL_SIZE=8
else
CCFLAGS := $(filter-out -pthread,$(CCFLAGS))

LDFLAGS := $(filter-out -pthread,$(LDFLAGS)) \
-sENVIRONMENT=web \
-sNO_ALLOW_BLOCKING_ON_MAIN_THREAD
endif
7 changes: 7 additions & 0 deletions src/engine/audio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ namespace
#if defined( ANDROID )
// Value greater than 1024 causes audio distortion on Android
int chunkSize = 1024;
#elif defined( __EMSCRIPTEN__ )
// When a WebAssembly app is running in a browser, no background threads (emulated by the web workers) have
// access to the audio. The audio can only be accessed (and feeded) from the main thread (when yielding to
// the browser's event loop). The chunk size should be large enough to avoid playback issues caused by other
// activities performed on the main thread. On the other hand, it shouldn't be too big, because it is possible
// to stop the audio playback only after the current chunk has been completely played.
int chunkSize = 8192;
#else
int chunkSize = 2048;
#endif
Expand Down
15 changes: 13 additions & 2 deletions src/engine/localevent.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/***************************************************************************
* fheroes2: https://github.com/ihhub/fheroes2 *
* Copyright (C) 2019 - 2024 *
* Copyright (C) 2019 - 2025 *
* *
* Free Heroes2 Engine: http://sourceforge.net/projects/fheroes2 *
* Copyright (C) 2008 by Andrey Afletdinov <[email protected]> *
Expand Down Expand Up @@ -1237,16 +1237,19 @@ bool LocalEvent::HandleEvents( const bool sleepAfterEventProcessing, const bool

renderRoi = fheroes2::getBoundaryRect( renderRoi, _mouseCursorRenderArea );

static_assert( globalLoopSleepTime == 1, "Since you have changed the sleep time, make sure that the sleep does not last too long." );

if ( sleepAfterEventProcessing ) {
if ( renderRoi != fheroes2::Rect() ) {
display.render( renderRoi );
}

#ifndef __EMSCRIPTEN__
// Make sure not to delay any further if the processing time within this function was more than the expected waiting time.
if ( eventProcessingTimer.getMs() < globalLoopSleepTime ) {
static_assert( globalLoopSleepTime == 1, "Make sure that you sleep for the difference between times since you change the sleep time." );
EventProcessing::EventEngine::sleep( globalLoopSleepTime );
}
#endif
}
else {
// Since rendering is going to be just after the call of this method we need to update rendering area only.
Expand All @@ -1255,6 +1258,14 @@ bool LocalEvent::HandleEvents( const bool sleepAfterEventProcessing, const bool
}
}

#ifdef __EMSCRIPTEN__
// When a WebAssembly app is running in a browser, the sleep time is used to perform various "background" operations on the main thread (such
// as feeding the audio streams) by yielding to the browser's event loop using the Emscripten Asyncify mechanism. Therefore, it is preferable
// to always force the main thread to fall asleep, otherwise, for example, the following deadlock is possible: the main thread waits in a loop
// for an audio playback to finish, but it never finishes because new chunks are not feeded to it, because the main thread never goes to sleep.
EventProcessing::EventEngine::sleep( globalLoopSleepTime );
#endif

return true;
}

Expand Down
7 changes: 4 additions & 3 deletions src/engine/screen.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/***************************************************************************
* fheroes2: https://github.com/ihhub/fheroes2 *
* Copyright (C) 2020 - 2023 *
* Copyright (C) 2020 - 2025 *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
Expand Down Expand Up @@ -196,13 +196,14 @@ namespace fheroes2

~Display() override = default;

// Render a full frame on screen.
// Render an entire frame on screen.
void render()
{
render( { 0, 0, width(), height() } );
}

void render( const Rect & roi ); // render a part of image on screen. Prefer this method over full image if you don't draw full screen.
// Render a part of frame on screen.
void render( const Rect & roi );

// Update the area which will be rendered on the next render() call.
void updateNextRenderRoi( const Rect & roi );
Expand Down
52 changes: 52 additions & 0 deletions src/engine/thread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,38 @@
#include <cassert>
#include <memory>

#if defined( __EMSCRIPTEN__ ) && !defined( __EMSCRIPTEN_PTHREADS__ )
namespace
{
class MutexUnlocker
{
public:
explicit MutexUnlocker( std::mutex & mutex )
: _mutex( mutex )
{
_mutex.unlock();
}

MutexUnlocker( const MutexUnlocker & ) = delete;

~MutexUnlocker()
{
_mutex.lock();
}

MutexUnlocker & operator=( const MutexUnlocker & ) = delete;

private:
std::mutex & _mutex;
};
}
#endif

namespace MultiThreading
{
void AsyncManager::createWorker()
{
#if !defined( __EMSCRIPTEN__ ) || defined( __EMSCRIPTEN_PTHREADS__ )
if ( !_worker ) {
_runFlag = true;
_worker = std::make_unique<std::thread>( AsyncManager::_workerThread, this );
Expand All @@ -37,10 +65,14 @@ namespace MultiThreading
_masterNotification.wait( lock, [this] { return !_runFlag; } );
}
}
#endif
}

void AsyncManager::stopWorker()
{
#if defined( __EMSCRIPTEN__ ) && !defined( __EMSCRIPTEN_PTHREADS__ )
assert( !_worker );
#else
if ( _worker ) {
{
const std::scoped_lock<std::mutex> lock( _mutex );
Expand All @@ -54,13 +86,33 @@ namespace MultiThreading
_worker->join();
_worker.reset();
}
#endif
}

void AsyncManager::notifyWorker()
{
_runFlag = true;

#if defined( __EMSCRIPTEN__ ) && !defined( __EMSCRIPTEN_PTHREADS__ )
assert( !_exitFlag );

while ( _runFlag ) {
const bool moreTasks = prepareTask();
if ( !moreTasks ) {
_runFlag = false;
}

{
// In accordance with the contract, the _mutex should NOT be acquired while
// calling the executeTask() - even if its acquisition is in fact a snake oil.
MutexUnlocker unlocker( _mutex );

executeTask();
}
}
#else
_workerNotification.notify_all();
#endif
}

void AsyncManager::_workerThread( AsyncManager * manager )
Expand Down
28 changes: 12 additions & 16 deletions src/fheroes2/ai/ai_planner_hero.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/***************************************************************************
* fheroes2: https://github.com/ihhub/fheroes2 *
* Copyright (C) 2024 *
* Copyright (C) 2024 - 2025 *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
Expand Down Expand Up @@ -2779,7 +2779,7 @@ bool AI::Planner::HeroesTurn( VecHeroes & heroes, uint32_t & currentProgressValu
bestHero = hero;
}

// This loop may take many time for computations so update hourglass grains animation.
// This loop may take many time for computations, so pump the event queue and update the animation of the hourglass grains.
status.drawAITurnProgress( currentProgressValue );
}

Expand All @@ -2791,13 +2791,11 @@ bool AI::Planner::HeroesTurn( VecHeroes & heroes, uint32_t & currentProgressValu

// Calculate turn progress taking into account that the current 'bestHero' most likely will end his turn
// and will be removed from 'availableHeroes' vector: we add 3/4 (not 1/2) to help rounding the result.
uint32_t progressValue = ( turnProgressScale * ( heroesToMoveTotalCount - static_cast<uint32_t>( availableHeroes.size() ) ) + 3 * heroesToMoveTotalCount )
/ ( 4 * heroesToMoveTotalCount )
+ startProgressValue;
if ( currentProgressValue < progressValue ) {
currentProgressValue = progressValue;
status.drawAITurnProgress( currentProgressValue );
}
currentProgressValue = std::max( currentProgressValue,
( turnProgressScale * ( heroesToMoveTotalCount - static_cast<uint32_t>( availableHeroes.size() ) ) + 3 * heroesToMoveTotalCount )
/ ( 4 * heroesToMoveTotalCount )
+ startProgressValue );
status.drawAITurnProgress( currentProgressValue );

if ( bestTargetIndex == -1 ) {
// Possibly heroes have nothing to do because one of them is blocking the way. Move a random hero randomly and see what happens.
Expand Down Expand Up @@ -2906,13 +2904,11 @@ bool AI::Planner::HeroesTurn( VecHeroes & heroes, uint32_t & currentProgressValu
[]( const Heroes * hero ) { return !hero->MayStillMove( false, false ); } ),
availableHeroes.end() );

progressValue = ( turnProgressScale * ( heroesToMoveTotalCount - static_cast<uint32_t>( availableHeroes.size() ) ) + heroesToMoveTotalCount )
/ ( 4 * heroesToMoveTotalCount )
+ startProgressValue;
if ( currentProgressValue < progressValue ) {
currentProgressValue = progressValue;
status.drawAITurnProgress( currentProgressValue );
}
currentProgressValue = std::max( currentProgressValue,
( turnProgressScale * ( heroesToMoveTotalCount - static_cast<uint32_t>( availableHeroes.size() ) ) + heroesToMoveTotalCount )
/ ( 4 * heroesToMoveTotalCount )
+ startProgressValue );
status.drawAITurnProgress( currentProgressValue );
}

status.drawAITurnProgress( endProgressValue );
Expand Down
10 changes: 5 additions & 5 deletions src/fheroes2/ai/ai_planner_kingdom.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/***************************************************************************
* fheroes2: https://github.com/ihhub/fheroes2 *
* Copyright (C) 2024 *
* Copyright (C) 2024 - 2025 *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
Expand Down Expand Up @@ -772,8 +772,8 @@ void AI::Planner::KingdomTurn( Kingdom & kingdom )

updateKingdomBudget( kingdom );

uint32_t progressStatus = 1;
status.drawAITurnProgress( progressStatus );
uint32_t currentProgressValue = 1;
status.drawAITurnProgress( currentProgressValue );

std::set<int> castlesInDanger;
std::vector<AICastle> sortedCastleList;
Expand Down Expand Up @@ -804,9 +804,9 @@ void AI::Planner::KingdomTurn( Kingdom & kingdom )
// If AI has less than three heroes at the start of the turn we assume
// that he will buy another one in this turn and allow progress to increase only for 2 points.
uint32_t const endProgressValue
= ( progressStatus == 1 ) ? std::min( static_cast<uint32_t>( heroes.size() ) * 2U + 1U, 8U ) : std::min( progressStatus + 2U, 9U );
= ( currentProgressValue == 1 ) ? std::min( static_cast<uint32_t>( heroes.size() ) * 2U + 1U, 8U ) : std::min( currentProgressValue + 2U, 9U );

bool moreTaskForHeroes = HeroesTurn( heroes, progressStatus, endProgressValue );
bool moreTaskForHeroes = HeroesTurn( heroes, currentProgressValue, endProgressValue );

// Step 4. Buy new heroes, adjust roles, sort heroes based on priority or strength
if ( purchaseNewHeroes( sortedCastleList, castlesInDanger, availableHeroCount, moreTaskForHeroes ) ) {
Expand Down
10 changes: 6 additions & 4 deletions src/fheroes2/gui/interface_status.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/***************************************************************************
* fheroes2: https://github.com/ihhub/fheroes2 *
* Copyright (C) 2019 - 2024 *
* Copyright (C) 2019 - 2025 *
* *
* Free Heroes2 Engine: http://sourceforge.net/projects/fheroes2 *
* Copyright (C) 2009 by Andrey Afletdinov <[email protected]> *
Expand Down Expand Up @@ -489,16 +489,18 @@ void Interface::StatusPanel::TimerEventProcessing()

void Interface::StatusPanel::drawAITurnProgress( const uint32_t progressValue )
{
// Even if there is no need to draw anything, we still need to pump the event queue to
// update the position of the software-emulated mouse cursor, feed the music player by
// another music chunk on some platforms (e.g. WebAssembly), etc.
LocalEvent::Get().HandleEvents( false );

const bool updateProgress = ( progressValue != _aiTurnProgress );
const bool isMapAnimation = Game::validateAnimationDelay( Game::MAPS_DELAY );

if ( !updateProgress && !isMapAnimation ) {
return;
}

// Process events if any before rendering a frame. For instance, updating a mouse cursor position.
LocalEvent::Get().HandleEvents( false );

if ( updateProgress ) {
if ( progressValue == 0 ) {
// If turn progress is just started start the grain animation from the beginning.
Expand Down

0 comments on commit 2498a89

Please sign in to comment.