diff --git a/README.md b/README.md index a019119..63b4bea 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Since this README is not included when importing gdCEF from the [Godot Asset Lib - [Installation](addons/gdcef/doc/installation.md) - [Releases](https://github.com/Lecrapouille/gdcef/releases) - [API](addons/gdcef/doc/API.md) +- [Architecture](addons/gdcef/doc/architecture.md) - [Details Design](addons/gdcef/doc/detailsdesign.md) - [Demos](addons/gdcef/demos/README.md) - [FAQ](addons/gdcef/README.md#faq) diff --git a/addons/gdcef/doc/architecture.md b/addons/gdcef/doc/architecture.md index 312c861..3a544b2 100644 --- a/addons/gdcef/doc/architecture.md +++ b/addons/gdcef/doc/architecture.md @@ -1,71 +1,106 @@ -# Software Architecture. - -**WIP: Consider this document as a draft!!!** - -This details design document is about [gdcef] -(https://github.com/Lecrapouille/gdcef) implementing a [Chromium Embedded -Framework](https://bitbucket.org/chromiumembedded/cef/wiki/Home) (CEF) native -module for the [Godot editor](https://godotengine.org/) (gdCef). - -Inside the `gdcef` folder, two main classes deriving from `godot::Node` have -been created to wrap the CEF C++ API to be usable from Godot scripts. Derived -from Godot Nodes, it allows instances of these classes to be attached inside to -the scene-graph as depicted by the following picture. - -![CEFnode](pics/cef.png) - -See this -[document](https://docs.godotengine.org/en/stable/classes/class_node.html) -concerning what a Godot Node is. - -## Classes Diagram - -The following picture depicts the class diagram: - -![classdiag](architecture/classes.png) - -- `GDCef` implemented in [gdcef/src/gdcef.cpp](gdcef.[ch]pp). Its goal is to - wrap up the initialization phase of CEF, its settings, and the loopback of - messages of CEF subprocesses. This class allows creating `GDBrowserView` - that are attached as child nodes inside the scene-graph. - -- `GDBrowserView` implemented in [gdcef/src/gdbrowser.cpp](gdbrowser.[ch]pp). - Its goal is to wrap a browser view allowing to display the web document, to - interact with the user (mouse, keyboard), to load pages, ... - -The [gdcef/src/gdlibrary.cpp](gdcef/src/gdlibrary.cpp) allows Godot to -register the two classes. See this document for more information: -https://docs.godotengine.org/en/stable/development/cpp/custom_modules_in_cpp.html - -## CEF Secondary process - -A [secondary CEF -process](https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-separate-sub-process-executable) -is needed when CEF (here, our class `GDCef`) cannot directly access the -`main(int argc, char* argv[])` function of the application. This is mandatory for -its initialization. - -This is, unfortunately, the case since CEF is created as a Node scene-graph -but CEF and its access to Godot Engine's `main` function do not exist -in Godot's source code. - -When starting, CEF will fork the application several times into processes -and the forked processes become specialized processes - -You have to know that CEF modifies the content of your `argv` and this may mess -up your application if it also parses the command line (you can back it up, -meaning using a `std::vector` to back up `argv` and after CEF init to restore -values in `argv` back). What is "two separated processes" exactly? Just an extra -fork: the main process forks itself and calls the secondary process, which can -fully access it is own main(int argc, char* argv[]). The main constraint is the -path of the secondary process shall be canonic (and this is a pain to get the -real path). - - +# Software Architecture + +This document details the software architecture of [gdcef](https://github.com/Lecrapouille/gdcef), a native module for the [Godot engine](https://godotengine.org/) that implements the [Chromium Embedded Framework](https://bitbucket.org/chromiumembedded/cef/wiki/Home) (CEF). + +## Overview + +The gdcef module consists of two main components: + +1. A main process that handles CEF initialization and browser management +2. A render process that handles web page rendering and JavaScript execution + +## Core Components + +### Main Process Classes + +Two main classes are exposed to Godot as Nodes: + +#### GDCef Class + +Implemented in `gdcef/src/gdcef.hpp`, this class serves as the entry point: + +- **GDCef**: The entry point class that: + - Initializes CEF and manages its lifecycle + - Handles CEF settings and configuration + - Creates and manages browser instances + - Routes messages between CEF subprocesses + +- **GDBrowserView**: Represents a browser instance that: + - Manages web page display and rendering + - Handles user interactions (mouse, keyboard) + - Controls page navigation and JavaScript execution + - Manages audio streaming + - Handles file downloads + +These classes are derived from `godot::Node`, allowing them to be integrated into Godot's scene tree: + +![CEF node integration](pics/cef.png) + +### Render Process + +The render process is implemented in a separate executable and handles: + +- Web page rendering +- JavaScript execution +- V8 context management +- Communication with the main process + +## Communication Flow + +### Initialization Sequence + +![Init Sequence](architecture/sequence_init.png) + +1. GDScript initializes GDCef with configuration +2. CEF initializes and forks required processes +3. The render process starts and initializes its components +4. Browser instances can then be created + +### Rendering Sequence + +![Paint Sequence](architecture/sequence_paint.png) + +1. The render process renders web content +2. Content is painted to an off-screen buffer +3. The buffer is converted to a Godot texture +4. The texture is displayed in the Godot scene + +### Download Sequence + +![Download Sequence](architecture/sequence_download.png) + +1. Download is initiated from browser +2. CEF handles the download process +3. Progress updates are sent to Godot +4. Download completion is signaled + +## Class Relationships + +The following diagram shows the relationships between the main components: + +![Class Diagram](architecture/classes.png) + +### Key Relationships + +- GDCef creates and manages GDBrowserView instances +- GDBrowserView communicates with CEF browser instances +- The render process communicates with both GDCef and browser instances +- All CEF-related classes implement appropriate CEF interfaces + +## Implementation Details + +- The module uses CEF's windowless rendering mode for seamless integration with Godot +- Audio is routed through Godot's audio system +- JavaScript integration allows bidirectional communication between Godot and web content +- File downloads are managed through CEF's download handler interface +- Mouse and keyboard events are translated from Godot to CEF format + +## Technical Constraints + +- CEF requires a separate render process executable +- The render process path must be canonical +- CEF modifies command line arguments during initialization +- Memory management must account for both Godot's reference counting and CEF's reference counting + +For API documentation and usage examples, please refer to [API.md](API.md). diff --git a/addons/gdcef/doc/architecture/class_diagram.png b/addons/gdcef/doc/architecture/class_diagram.png new file mode 100644 index 0000000..1a80e52 Binary files /dev/null and b/addons/gdcef/doc/architecture/class_diagram.png differ diff --git a/addons/gdcef/doc/architecture/class_diagram.puml b/addons/gdcef/doc/architecture/class_diagram.puml new file mode 100644 index 0000000..92a437a --- /dev/null +++ b/addons/gdcef/doc/architecture/class_diagram.puml @@ -0,0 +1,198 @@ +@startuml + +skinparam class { + BackgroundColor White + ArrowColor Black + BorderColor Black +} + +package "CEF Framework" #LightBlue { + namespace cef { + interface CefApp { + {abstract} + +GetRenderProcessHandler() + } + + interface CefClient { + {abstract} + +GetRenderHandler() + +GetLifeSpanHandler() + +GetLoadHandler() + } + + interface CefRenderProcessHandler { + {abstract} + +OnContextCreated() + } + + interface CefV8Handler { + {abstract} + +Execute() + } + + interface CefRenderHandler { + {abstract} + +GetViewRect() + +OnPaint() + } + } +} + +package "Godot Framework" #LightGreen { + namespace godot { + class Object { + {abstract} + } + class Node { + {abstract} + } + class TextureRect { + } + + Node --|> Object + TextureRect --|> Node + } +} + +package "GDCEF Core" #LightYellow { + class GDCef { + -m_impl: GDCef::Impl* + -m_cef_settings: CefSettings + +initialize(config: Dictionary): bool + +shutdown(): void + +create_browser(): GDBrowserView* + +isAlive(): bool + +get_error(): String + +get_full_version(): String + +get_version_part(entry: int): int + +register_method(godot_object: Object, browser: GDBrowserView, method_name: String): void + } + + class "GDCef::Impl" as GDCefImpl { + -m_gdcef: GDCef& + +OnBeforeCommandLineProcessing() + +OnAfterCreated() + +DoClose() + +OnBeforeClose() + +closeAllBrowsers() + } + + GDCef --|> godot::Node + GDCef *-- GDCefImpl + GDCefImpl ..|> cef::CefApp +} + +package "GDCEF Browser" #LightPink { + class GDBrowserView { + -m_impl: GDBrowserView::Impl* + -m_texture: Ref + +init(url: String): int + +load_url(url: String): void + +resize(size: Vector2): void + +execute_javascript(code: String): void + +close(): void + +id(): int + +get_error(): String + +is_valid(): bool + +set_texture(texture: ImageTexture): void + +get_texture(): ImageTexture + +set_zoom_level(level: float): void + +get_title(): String + +get_url(): String + +load_data_uri(html: String, mime_type: String): void + +download_file(url: String): void + +allow_downloads(allow: bool): void + +set_download_folder(path: String): void + +is_loaded(): bool + +reload(): bool + +stop_loading(): void + +copy(): void + +paste(): void + +undo(): void + +redo(): void + +request_html_content(): void + +has_previous_page(): bool + +has_next_page(): bool + +previous_page(): void + +next_page(): void + +set_viewport(x: float, y: float, w: float, h: float): bool + +set_key_pressed(key: int): void + +set_mouse_moved(x: int, y: int): void + +set_mouse_left_click(x: int, y: int): void + +set_mouse_right_click(x: int, y: int): void + +set_mouse_middle_click(x: int, y: int): void + +set_mouse_left_down(x: int, y: int): void + +set_mouse_left_up(x: int, y: int): void + +set_mouse_right_down(x: int, y: int): void + +set_mouse_right_up(x: int, y: int): void + +set_mouse_middle_down(x: int, y: int): void + +set_mouse_middle_up(x: int, y: int): void + +set_mouse_wheel_vertical(delta: int): void + +set_mouse_wheel_horizontal(delta: int): void + +set_muted(mute: bool): bool + +is_muted(): bool + +set_audio_stream(audio: AudioStreamGeneratorPlayback): void + +get_audio_stream(): AudioStreamGeneratorPlayback + +get_pixel_color(x: int, y: int): Color + } + + class "GDBrowserView::Impl" as GDBrowserViewImpl { + -m_browser: CefRefPtr + -m_texture_rect: TextureRect* + +GetViewRect() + +OnPaint() + +OnLoadEnd() + +OnLoadError() + +OnBeforePopup() + +OnBeforeDownload() + +OnDownloadUpdated() + +OnProcessMessageReceived() + +OnAudioStreamStarted() + +OnAudioStreamPacket() + } + + GDBrowserView --|> godot::Node + GDBrowserView *-- GDBrowserViewImpl + GDBrowserView o-- godot::TextureRect + GDBrowserViewImpl ..|> cef::CefClient + GDBrowserViewImpl ..|> cef::CefRenderHandler +} + +package "Render Process" #LightGray { + class RenderProcess { + -m_handler: CefRefPtr + +GetRenderProcessHandler() + +OnContextCreated() + } + + class GodotMethodHandler { + -m_browser: CefRefPtr + +Execute() + } + + RenderProcess ..|> cef::CefApp + RenderProcess ..|> cef::CefRenderProcessHandler + RenderProcess *-- GodotMethodHandler + GodotMethodHandler ..|> cef::CefV8Handler + + ' Interactions avec GDCEF Core + RenderProcess ..> GDCef : "Communique avec" + GodotMethodHandler ..> GDCef : "Envoie des messages à" +} + +note top of GDCef + Main entry point that initializes CEF + and manages browser instances +end note + +note top of GDBrowserView + Represents a browser instance + with its own texture +end note + +note top of RenderProcess + Manages the render process + and JavaScript bindings +end note + +@enduml \ No newline at end of file diff --git a/addons/gdcef/doc/architecture/classes.plantuml b/addons/gdcef/doc/architecture/classes.plantuml deleted file mode 100644 index 570f040..0000000 --- a/addons/gdcef/doc/architecture/classes.plantuml +++ /dev/null @@ -1,15 +0,0 @@ -@startuml - -class Node as "godot::Node" - -class GDCef { -+ void _process(float delta) -+ GDBrowserView* createBrowser(...) -+ void shutdown() -} - -Node <|-- GDCef -Node <|-- GDBrowserView -GDCef -> "*" GDBrowserView - -@enduml diff --git a/addons/gdcef/doc/architecture/classes.png b/addons/gdcef/doc/architecture/classes.png deleted file mode 100644 index e64ad5f..0000000 Binary files a/addons/gdcef/doc/architecture/classes.png and /dev/null differ diff --git a/addons/gdcef/doc/architecture/sequence_download.png b/addons/gdcef/doc/architecture/sequence_download.png new file mode 100644 index 0000000..35f16e9 Binary files /dev/null and b/addons/gdcef/doc/architecture/sequence_download.png differ diff --git a/addons/gdcef/doc/architecture/sequence_download.puml b/addons/gdcef/doc/architecture/sequence_download.puml new file mode 100644 index 0000000..884e461 --- /dev/null +++ b/addons/gdcef/doc/architecture/sequence_download.puml @@ -0,0 +1,42 @@ +@startuml + +participant "GDScript\nTextureRect" as GD +participant "GDCef\n(Main Process)" as MP +participant "CEF" as CEF +participant "Render Process" as RP + +== Initial Setup == + +GD -> MP : resize(size) +activate MP +MP -> CEF : GetHost()->WasResized() +deactivate MP + +== File Download == + +GD -> MP : load_url(download_url) +activate MP +MP -> CEF : LoadURL() +activate CEF + +CEF -> RP : Start download +activate RP + +loop During Download + RP -> CEF : OnDownloadUpdated() + CEF -> MP : Download progress + MP -> GD : emit_signal("on_download_updated", file_path, download_progress, browser_ptr) + note right: file_path: path of the file\ndownload_progress: percentage\nbrowser_ptr: pointer to the browser +end + +alt Download Complete + RP -> CEF : OnDownloadComplete() + CEF -> MP : Download finished + MP -> GD : emit_signal("on_download_updated", file_path, 100, browser_ptr) +end + +deactivate RP +deactivate CEF +deactivate MP + +@enduml \ No newline at end of file diff --git a/addons/gdcef/doc/architecture/sequence_init.png b/addons/gdcef/doc/architecture/sequence_init.png new file mode 100644 index 0000000..6945a4f Binary files /dev/null and b/addons/gdcef/doc/architecture/sequence_init.png differ diff --git a/addons/gdcef/doc/architecture/sequence_init.puml b/addons/gdcef/doc/architecture/sequence_init.puml new file mode 100644 index 0000000..6e580e0 --- /dev/null +++ b/addons/gdcef/doc/architecture/sequence_init.puml @@ -0,0 +1,53 @@ +@startuml + +participant "GDScript" as GD +participant "GDCef\n(Main Process)" as MP +participant "CEF" as CEF +participant "Render Process" as RP + +== Initialization == + +GD -> MP : initialize(config) +activate MP + +MP -> CEF : CefInitialize() +activate CEF + +CEF -> CEF : Fork processes +note right: Creates render, GPU,\nand other processes + +CEF -> RP : Start render process +activate RP + +RP -> RP : CefExecuteProcess() +RP -> RP : CefInitialize() +RP -> RP : CefRunMessageLoop() + +CEF --> MP : Initialization complete +deactivate CEF + +MP --> GD : true +deactivate MP + +== Browser Creation == + +GD -> MP : create_browser(url, texture_rect, settings) +activate MP + +MP -> CEF : CreateBrowserSync() +activate CEF + +CEF -> RP : OnContextCreated() +activate RP +RP -> RP : Create V8 context +RP -> RP : Bind JavaScript methods +RP --> CEF : Context ready +deactivate RP + +CEF --> MP : Browser instance +deactivate CEF + +MP --> GD : GDBrowserView instance +deactivate MP + +@enduml \ No newline at end of file diff --git a/addons/gdcef/doc/architecture/sequence_js_communication.png b/addons/gdcef/doc/architecture/sequence_js_communication.png new file mode 100644 index 0000000..3a7df2f Binary files /dev/null and b/addons/gdcef/doc/architecture/sequence_js_communication.png differ diff --git a/addons/gdcef/doc/architecture/sequence_js_communication.puml b/addons/gdcef/doc/architecture/sequence_js_communication.puml new file mode 100644 index 0000000..94bfb08 --- /dev/null +++ b/addons/gdcef/doc/architecture/sequence_js_communication.puml @@ -0,0 +1,60 @@ +@startuml + +participant "JavaScript" as JS +participant "Render Process" as RP +participant "CEF" as CEF +participant "GDCef\n(Main Process)" as MP +participant "GDScript" as GD + +== JavaScript to GDScript == + +JS -> RP : godot.callGodotMethod() +activate RP + +RP -> RP : GodotMethodHandler::Execute() +RP -> CEF : SendProcessMessage(PID_BROWSER) +activate CEF + +CEF -> MP : OnProcessMessageReceived() +activate MP + +MP -> GD : Call GDScript method +activate GD +GD --> MP : Return value +deactivate GD + +MP --> CEF : Process message result +deactivate MP + +CEF --> RP : Message processed +deactivate CEF + +RP --> JS : JavaScript return value +deactivate RP + +== GDScript to JavaScript == + +GD -> MP : execute_javascript() +activate MP + +MP -> CEF : ExecuteJavaScript() +activate CEF + +CEF -> RP : Execute in V8 context +activate RP + +RP -> JS : Execute JavaScript code +activate JS +JS --> RP : JavaScript result +deactivate JS + +RP --> CEF : Execution complete +deactivate RP + +CEF --> MP : JavaScript executed +deactivate CEF + +MP --> GD : Execution complete +deactivate MP + +@enduml \ No newline at end of file diff --git a/addons/gdcef/doc/architecture/sequence_paint.png b/addons/gdcef/doc/architecture/sequence_paint.png new file mode 100644 index 0000000..5bf9292 Binary files /dev/null and b/addons/gdcef/doc/architecture/sequence_paint.png differ diff --git a/addons/gdcef/doc/architecture/sequence_paint.puml b/addons/gdcef/doc/architecture/sequence_paint.puml new file mode 100644 index 0000000..80affab --- /dev/null +++ b/addons/gdcef/doc/architecture/sequence_paint.puml @@ -0,0 +1,78 @@ +@startuml + +participant "GDScript,\nTextureRect" as GD +participant "gdCEF" as MP +participant "CEF\nMain Process" as CEF +participant "CEF\nRender Process" as RP + +== Browser Navigation Success == + +GD -> MP : load_url(url) +activate MP +MP -> CEF : LoadURL() +activate CEF + +CEF -> RP : Load and render page +activate RP +RP -> RP : Process page +note right: HTML parsing,\nJS execution,\nCSS rendering + +RP -> CEF : OnLoadEnd() +CEF -> MP : Page loaded +MP -> GD : emit_signal("on_page_loaded") +deactivate RP +deactivate CEF +deactivate MP + +== Browser Navigation Failure == + +GD -> MP : load_url(url) +activate MP +MP -> CEF : LoadURL() +activate CEF + +CEF -> RP : Load and render page +activate RP +RP -> RP : Process page fails +note right: Network error,\n404 not found,\nInvalid URL... + +RP -> CEF : OnLoadError() +CEF -> MP : Load failed +MP -> GD : emit_signal("on_page_failed_loading", error_code, error_text, failed_url) +deactivate RP +deactivate CEF +deactivate MP +activate GD +GD -> GD : Encode HTML as data URI +note right: Format: data:text/html;base64,... + +GD -> MP : load_data_uri(data_uri, "text/html") +MP -> CEF: LoadURL() +note right: Same sequence depicted above +deactivate GD + +== Rendering Cycle == + +RP -> RP : Render webpage +activate RP +note right: Off-screen rendering\nin windowless mode + +RP -> CEF : OnPaint() +activate CEF + +CEF -> MP : Paint handler +activate MP + +MP -> MP : Update texture buffer +note right: Copy rendered content\nto texture memory + +MP -> GD : Updated texture +activate GD +GD -> GD : Display new frame +deactivate GD + +deactivate MP +deactivate CEF +deactivate RP + +@enduml \ No newline at end of file