Raytracer docs 🌐 EN

Accueil / Rendu en cluster

Rendu en cluster

Le module cluster (srcs/core/cluster/*, interfaces srcs/common/cluster/*) permet de répartir le rendu d’une image par tuiles sur plusieurs machines du réseau. Une machine joue le rôle de serveur : elle détient la scène et distribue les tuiles à rendre. Les autres machines sont des workers (clients) : elles calculent les tuiles reçues et renvoient les pixels au serveur, qui assemble l’image finale.

Vue d’ensemble

Le but du cluster est d’accélérer un rendu en faisant travailler plusieurs ordinateurs en parallèle. L’image est découpée en tuiles rectangulaires (taille par défaut 32×32), chacune identifiée par un tile_id. Le serveur maintient une file de tuiles à faire et les envoie aux workers connectés ; chaque worker rend sa tuile localement puis renvoie le tableau de pixels correspondant. Le serveur accumule ces pixels dans le tampon d’image.

Le rendu est progressif par échantillons (sample) : chaque tuile est associée à un numéro d’échantillon, et une nouvelle passe complète peut être relancée pour affiner le résultat. La coordination des tuiles est gérée par ClusterRenderCoordinator, et l’accumulation par ClusterRenderer (qui implémente à la fois ISceneRenderer et IClusterTileSink).

Le mode cluster est optionnel et s’active en ligne de commande (--server / --connect) ou depuis l’interface. Sans ces options, le Raytracer fonctionne en solo, exactement comme décrit dans le pipeline de rendu.

Serveur et workers

Le module distingue deux rôles, exposés par ClusterMode (CLIENT, SERVER, NONE) :

RôleClasseResponsabilités
Serveur (hôte) ClusterServer (server/*) Détient la scène, écoute les connexions TCP entrantes, gère une liste de Connection, distribue les tuiles (dispatchRenderRequests), reçoit les pixels et diffuse l’état de rendu à tous les workers.
Worker (client) ClusterClient (client/*) Se connecte au serveur, récupère la scène, attend des demandes de tuiles, rend chaque tuile dans un thread dédié (renderLoop) et renvoie les pixels.

Côté serveur, chaque worker est représenté par une Connection qui suit un ConnectionState (PENDING, CONNECTED, DISCONNECTED, REFUSED). Côté worker, le client suit son propre ClientState (au fil de la session : récupération des données, réception, rendu, envoi, repos) et l’état de rendu du serveur (ServerRenderState : IDLING ou RENDERING).

API — IClusterModule

Le point d’entrée est l’interface IClusterModule (srcs/common/cluster/IClusterModule.hpp), implémentée par ClusterModule. Elle possède un serveur ou un client (jamais les deux à la fois) et refuse de démarrer si un mode est déjà actif.

namespace rc
{
    enum class ClusterMode { CLIENT, SERVER, NONE };

    class IClusterModule
    {
        public:
            virtual ClusterMode   getClusterMode()   const = 0;
            virtual IClusterServer *getClusterServer() const = 0;
            virtual IClusterClient *getClusterClient() const = 0;

            // Démarre un serveur qui détient `scene` ; port 0 = port automatique.
            virtual void startServer(IScene *scene, size_t port = 0) = 0;

            // Rejoint un serveur en tant que worker.
            virtual void joinCluster(const std::string &address, size_t port) = 0;

            // Quitte le cluster (uniquement valable en mode CLIENT).
            virtual void leaveCluster() = 0;
    };
}
MéthodeEffet
startServer(scene, port)Crée un ClusterServer pour la scène et le démarre ; passe en mode SERVER. Lève une exception si un cluster est déjà actif ou si le démarrage échoue.
joinCluster(address, port)Crée un ClusterClient et se connecte à address:port ; passe en mode CLIENT. Lève une exception en cas d’échec de connexion.
leaveCluster()Déconnecte et détruit le client, repasse en mode NONE. Lève une exception si l’on n’est pas connecté en tant que client.
getClusterMode()Renvoie le mode courant (CLIENT / SERVER / NONE).
getClusterServer() / getClusterClient()Accès à l’instance active (ou nullptr).

Déroulé d’une session

La communication passe par des sockets TCP. Chaque message est encadré par un en-tête de 6 octets (identifiant de paquet sur 2 octets + taille de la charge utile sur 4 octets, en ordre réseau), suivi de la charge utile sérialisée. Le déroulé nominal d’une session est le suivant :

  1. Connexion — le worker ouvre une connexion TCP vers le serveur et envoie ClientJoinRequest.
  2. Acceptation — le serveur marque la connexion CONNECTED et répond ServerJoinResponse.
  3. Demande de scène — le worker envoie ClientFetchSceneData.
  4. Envoi de la scène — le serveur sérialise la scène en JSON (SceneRegister) et renvoie ServerSceneData.
  5. Préparation — le worker analyse le JSON (SceneParser), construit le BVH et passe au repos (IDLING).
  6. Distribution — pendant le rendu, le serveur assigne des tuiles avec ServerRenderRequest (identifiant, échantillon, coordonnées).
  7. Retour des pixels — le worker rend la tuile et renvoie ClientTileData (les pixels de la tuile).
  8. Assemblage — le serveur valide la tuile (markComplete) et accumule les pixels dans l’image (applyTileSample).
  9. État & annulation — le serveur diffuse son état (ServerRenderState) et peut annuler les tuiles en cours (ServerCancelRender).
Worker (client)                         Serveur (détient la scène)
     |                                            |
     |  1. connexion TCP + ClientJoinRequest      |
     | -----------------------------------------> |
     |                                            |  → CONNECTED
     |        2. ServerJoinResponse               |
     | <----------------------------------------- |
     |                                            |
     |     3. ClientFetchSceneData                |
     | -----------------------------------------> |
     |                                            |
     |     4. ServerSceneData (scène en JSON)     |
     | <----------------------------------------- |
     |  (parseScene + buildBvh, état IDLING)      |
     |                                            |
     |     5. ServerRenderRequest (tuile)         |
     | <----------------------------------------- |
     |  (render_tile_sample sur la tuile)         |
     |     6. ClientTileData (pixels)             |
     | -----------------------------------------> |
     |                                            |  markComplete + applyTileSample
     |          ...  autres tuiles  ...           |
     |   ServerRenderState / ServerCancelRender   |
     | <----------------------------------------- |

Une file de tuiles avec délai de garde (requeueTimedOut, ~5 s) permet de réassigner à un autre worker une tuile qui n’est pas revenue à temps, ce qui rend le rendu tolérant aux workers lents ou perdus.

Types de paquets

Les paquets sont typés par PacketID et reconstruits à la réception par PacketFactory::createPacket(id, payload). Chaque paquet implémente IPacket (serialize / deserialize / handle / getId) et s’auto-traite via un IPacketHandler (la connexion côté serveur, le client côté worker). Le tableau ci-dessous liste les paquets réellement enregistrés dans la fabrique :

PaquetSensContenu
PacketClientJoinRequestworker → serveurDemande de connexion (charge utile vide).
PacketServerJoinResponseserveur → workerAcceptation de la connexion (charge utile vide).
PacketClientFetchSceneDataworker → serveurDemande d’envoi de la scène (charge utile vide).
PacketServerSceneDataserveur → workerScène complète sérialisée en JSON (sceneData).
PacketServerClusterDataserveur → workerNombre de workers connectés (nb_clients).
PacketServerRenderRequestserveur → workerDemande de rendu d’une tuile : tile_id, sample, start_x, start_y, end_x, end_y.
PacketClientTileDataworker → serveurPixels calculés d’une tuile : mêmes identifiants/coordonnées + tableau pixels (ColorF).
PacketServerRenderStateserveur → workerÉtat de rendu du serveur (IDLING / RENDERING).
PacketServerCancelRenderserveur → workerAnnulation du rendu en cours (charge utile vide).

L’énumération PacketID réserve aussi deux identifiants (CLIENT_FETCH_CLUSTER_DATA et SERVER_TILE_DATA) qui ne sont pas encore enregistrés dans PacketFactory — ils n’ont donc pas d’effet à ce stade.

Format sur le fil (identique dans les deux sens) :

┌───────────────┬────────────────────┬───────────────────────┐
│ id (2 octets) │ taille (4 octets)  │ charge utile (N octets)│
└───────────────┴────────────────────┴───────────────────────┘
      uint16            uint32              sérialisation IPacket
   (ordre réseau)   (ordre réseau)

Démarrer un cluster

Les options sont analysées par srcs/config/Options.hpp. Le fichier de scène est un argument positionnel optionnel ; sans lui, une scène par défaut est chargée.

Sur la machine serveur (elle détient la scène) :

# Démarre l'interface et un serveur de cluster sur le port 8080
./raytracer --server 8080 scenes/demo.cfg

# Port automatique (choisi par le système) si l'on omet le numéro
./raytracer --server scenes/demo.cfg

Sur chaque worker (il rejoint le serveur) :

# Rejoint le serveur situé à l'IP et au port indiqués
./raytracer --connect 192.168.1.10 8080
OptionEffet
--server [PORT]Lance l’interface et démarre le serveur (startServer). PORT est optionnel (défaut : port automatique).
--connect IP PORTLance l’interface et rejoint le serveur IP:PORT (joinCluster). IP et PORT sont obligatoires.

Règles d’exclusion. Le mode headless -r / --render ne peut pas être combiné avec --server ni --connect (le cluster nécessite l’interface). De plus, --server et --connect sont mutuellement exclusifs — une instance est serveur ou worker, jamais les deux.

Rejoindre depuis l’interface

En plus de la ligne de commande, on peut rejoindre un cluster depuis l’interface graphique via la fenêtre JoinClusterWindow (srcs/plugins/user_interface/windows/JoinClusterWindow.hpp). C’est une petite fenêtre « Join a cluster » (400×180) proposant :

  • un champ IP (adresse du serveur) ;
  • un champ Port, restreint aux entiers non signés ;
  • un bouton JOIN.

La validation d’un champ ou le clic sur JOIN déclenche un rappel windowCallback(ip, port) qui appelle in fine joinCluster(ip, port) sur le module. Valider le champ IP fait passer le focus au champ Port, et le port doit être renseigné pour que la connexion soit tentée.

Limites & comportement

D’après le code actuel :

  • IPv4 uniquement — les sockets utilisent AF_INET / inet_pton(AF_INET, …) ; les adresses IPv6 ne sont pas gérées.
  • Transport en clair — communication TCP brute, sans authentification ni chiffrement. À réserver à un réseau local de confiance.
  • Scène transmise en JSON — le serveur re-sérialise la scène via SceneRegister et le worker la reconstruit avec SceneParser ; chaque worker reconstruit son propre BVH à la réception.
  • Tolérance aux workers lents — une tuile non revenue avant le délai de garde (~5 s) est réassignée ; la gestion fine des déconnexions en cours de rendu reste minimale (handleClientDisconnect est actuellement un point d’extension quasi vide).
  • Un seul rôle à la fois — le module refuse de démarrer un serveur ou de rejoindre un cluster s’il est déjà en mode SERVER ou CLIENT.
  • Rendu piloté par le serveur — les workers ne rendent que les tuiles que le serveur leur assigne ; ils restent au repos tant qu’aucune demande n’arrive.