phd-typst/foreword/implementation.typ

174 lines
10 KiB
XML

= Implementation details
During this thesis, a lot of software has been developed, and for this software to be successful and efficient, we chose appropriate languages.
When it comes to 3D streaming systems, we need two kind of software.
- *Interactive applications* which can run on as many devices as possible so we can easily conduct user studies. For this context, we chose the *JavaScript* language, since it can run on many devices and it has great support for WebGL.
- *Native applications* which can run fast on desktop devices, in order to prepare data, run simulations and evaluate our ideas. For this context, we chose the *Rust* language, which is a somewhat recent language that provides both the efficiency of C and C++ and the safety of functional languages.
== JavaScript
=== THREE.js
On the web browser, it is now possible to perform 3D rendering by using WebGL.
However, WebGL is very low level and it can be painful to write code, even to render a simple triangle.
For example, #link("https://www.tutorialspoint.com/webgl/webgl_drawing_a_triangle.htm")[this tutorial]'s code contains 121 lines of JavaScript, 46 being code (not comments or empty lines) to render a simple, non-textured triangle.
For this reason, it seems unreasonable to build a system like the one we are describing in raw WebGL.
There are many libraires that wrap WebGL code and that help people building 3D interfaces, and
#link("https://threejs.org")[THREE.js] is a very popular one (56617 stars on github, making it the 35th most starred repository on GitHub as of November 26th, 2019) // \footnote{\url{https://web.archive.org/web/20191126151645/https://gitstar-ranking.com/mrdoob/three.js}}). // TODO footnote
THREE.js acts as a 3D engine built on WebGL.
It provides classes to deal with everything we need:
- the *Renderer* class contains all the WebGL code needed to render a scene on the web page;
- the *Object* class contains all the boilerplate needed to manage the tree structure of the content, it contains a transform (translation and rotation) and it can have children that are other objects;
- the *Scene* class is the root object, it contains all of the objects we want to render and it is passed as argument to the render function;
- the *Geometry* and *BufferGeometry* classes are the classes that hold the vertex buffers, we will discuss it more in the next paragraph;
- the *Material* class is the class that holds the properties used to render geometry (the most important information being the texture), there are many classes derived from Material, and the developer can choose what material they want for their objects;
- the *Mesh* class is the class that links the geometry and the material, it derives the Object class and can thus be added to a scene and rendered.
A snippet of the basic usage of these classes is given in @three-hello-world.
#figure(
align(left,
raw(
read("../assets/dash-3d-implementation/base.js"),
block: true,
lang: "js",
),
),
caption: [A THREE.js _hello world_]
)<three-hello-world>
=== Geometries
Geometries are the classes that hold the vertices, texture coordinates, normals and faces.
THREE.js proposes two classes for handling geometries:
- the *Geometry* class, which is made to be developer friendly and allows easy editing but can suffer from performance issues;
- the *BufferGeometry* class, which is harder to use for a developer, but allows better performance since the developer controls how data is transmitted to the GPU.
== Rust
In this section, we explain the specificities of Rust and why it is an adequate language for writing efficient native software safely.
=== Borrow checker
Rust is a system programming language focused on safety.
It is made to be efficient (and effectively has performances comparable to C // TODO \footnote{\url{https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/rust.html}} or C++\footnote{\url{https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/rust-gpp.html}})
but with some extra features.
C++ users might see it as a language like C++ but that forbids undefined behaviours.// TODO \footnote{in Rust, when you need to execute code that might lead to undefined behaviours, you need to put it inside an \texttt{unsafe} block. Many operations are not available outside an \texttt{unsafe} block (e.g., dereferencing a pointer, or mutating a static variable). The idea is that you can use \texttt{unsafe} blocks when you require it, but you should avoid it as much as possible and when you do it, you must be particularly careful.}
The most powerful concept from Rust is _ownership_.
Basically, every value has a variable that we call its _owner_.
To be able to use a value, you must either be its owner or borrow it.
There are two types of borrow, the immutable borrow and the mutable borrow (roughly equivalent to references in C++).
The compiler comes with the _borrow checker_ which makes sure you only use variables that you are allowed to use.
For example, the owner can only use the value if it is not being borrowed, and it is only possible to either mutably borrow a value once, or immutably borrow a value many times.
At first, the borrow checker seems particularly efficient to detect bugs in concurrent software, but in fact, it is also decisive in non concurrent code.
Consider the piece of C++ code in @undefined-behaviour-cpp and @undefined-behaviour-it-cpp.
#figure(
align(left,
raw(
read("../assets/dash-3d-implementation/undefined-behaviour.cpp"),
block: true,
lang: "cpp",
),
),
caption: [Undefined behaviour with for each syntax],
)<undefined-behaviour-cpp>
#figure(
align(left,
raw(
read("../assets/dash-3d-implementation/undefined-behaviour-it.cpp"),
block: true,
lang: "cpp",
),
),
caption: [Undefined behaviour with iterator syntax],
)<undefined-behaviour-it-cpp>
This loop should go endlessly because the vector grows in size as we add elements in the loop.
But the most important thing here is that since we add elements to the vector, it will eventually need to be reallocated, and that reallocation will invalidate the iterator, meaning that the following iteration will provoke an undefined behaviour.
The equivalent code in Rust is in @undefined-behaviour-rs and @undefined-behaviour-it-rs.
#grid(
columns: (1fr, 0.2fr, 1fr),
align(center + horizon)[
#figure(
align(left,
raw(
read("../assets/dash-3d-implementation/undefined-behaviour.rs"),
block: true,
lang: "rust",
),
),
caption: [Rust version of @undefined-behaviour-cpp],
)<undefined-behaviour-rs>
],
[],
align(center + horizon)[
#figure(
align(left,
raw(
read("../assets/dash-3d-implementation/undefined-behaviour-it.rs"),
block: true,
lang: "rust",
),
),
caption: [Rust version of @undefined-behaviour-it-cpp],
)<undefined-behaviour-it-rs>
]
)
What happens is that the iterator needs to borrow the vector.
Because it is borrowed, it can no longer be borrowed as mutable since mutating it could invalidate the other borrowers.
And effectively, the borrow checker will crash the compiler with the error in @undefined-behaviour-error.
#figure(
align(left,
raw(
read("../assets/dash-3d-implementation/undefined-behaviour-error.txt"),
block: true,
),
),
caption: [Error given by the compiler on @undefined-behaviour-rs],
)<undefined-behaviour-error>
This example is one of the many examples of how powerful the borrow checker is: in Rust code, there can be no dangling reference, and all the segmentation faults coming from them are detected by the compiler.
The borrow checker may seem like an enemy to newcomers because it often rejects code that seem correct, but once they get used to it, they understand what is the problem with their code and either fix the problem easily, or realize that the whole architecture is wrong and understand why.
It is probably for those reasons that Rust is the _most loved programming language_ according to the Stack Overflow
Developer Survey // TODO in~\citeyear{so-survey-2016}, \citeyear{so-survey-2017}, \citeyear{so-survey-2018} and~\citeyear{so-survey-2019}.
=== Tooling
Moreover, Rust comes with many programs that help developers.
- #link("https://github.com/rust-lang/rust")[*`rustc`*] is the Rust compiler. It is comfortable due to the clarity and precise explanations of its error messages.
- #link("https://github.com/rust-lang/cargo")[*`cargo`*] is the official Rust's project and package manager. It manages compilation, dependencies, documentation, tests, etc.
- #link("https://github.com/racer-rust/racer")[*`racer`*], #link("https://github.com/rust-lang/rls")[*`rls`*] (Rust
Language Server) and #link("https://github.com/rust-analyzer/rust-analyzer")[*`rust-analyzer`*] are software that manage automatic compilation to display errors in code editors as well as providing semantic code completion.
- #link("https://github.com/rust-lang/rustfmt")[*`rustfmt`*] auto formats code.
- #link("https://github.com/rust-lang/rust-clippy")[*`clippy`*] is a linter that detects unidiomatic code and suggests modifications.
=== Glium
When we need to perform rendering for 3D content analysis or for evaluation, we use the #link("https://github.com/glium/glium")[`glium`] library.
Glium has many advantages over using raw OpenGL calls.
Its objectives are:
- to be easy to use: it exposes functions that are higher level than raw OpenGL calls, but still low enough level to let the developer free;
- to be safe: debugging OpenGL code can be a nightmare, and glium does its best to use the borrow checker to its advantage to avoid OpenGL bugs;
- to be fast: the binary produced use optimized OpenGL functions calls;
- to be compatible: glium seeks to support the latest versions of OpenGL functions and falls back to older functions if the most recent ones are not supported on the device.
=== Conclusion
In our work, many tasks will consist in 3D content analysis, reorganization, rendering and evaluation.
Many of these tasks require long computations, lasting from hours to entire days.
To perform them, we need a programming language that has good performances.
In addition, the extra features that Rust provides ease tremendously development, and this is why we use Rust for all tasks that do not require having a web interface.