<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[pali pi sona pi ilo nanpa]]></title><description><![CDATA[pali pi sona pi ilo nanpa]]></description><link>https://pbhnblog.ballif.eu</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 03:07:48 GMT</lastBuildDate><atom:link href="https://pbhnblog.ballif.eu/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Part 5: The Volume of Fluid method]]></title><description><![CDATA[In this post, I'll walk through my implementation of the Volume of Fluid method. The overall motivation is to simulate 3D waves and droplets of water, like this:


In practice, this post will mostly d]]></description><link>https://pbhnblog.ballif.eu/part-5-the-volume-of-fluid-method</link><guid isPermaLink="true">https://pbhnblog.ballif.eu/part-5-the-volume-of-fluid-method</guid><dc:creator><![CDATA[Pierre Ballif]]></dc:creator><pubDate>Fri, 03 Apr 2026 17:29:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6872696ce659b1dad2c6a371/162f7682-da1e-4b12-9860-fd90d000cbac.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, I'll walk through my implementation of the Volume of Fluid method. The overall motivation is to simulate 3D waves and droplets of water, like this:</p>
<img src="https://c.pxhere.com/photos/16/33/water_drops_liquid_abstracts_macro_falling_droplets_splashes-1237531.jpg!d" alt="" style="display:block;margin:0 auto" />

<p>In practice, this post will mostly describe "why are the incompressible Euler equations useful and how can we simulate them?"</p>
<h2>Models</h2>
<p>There is one reality, and there are different models of it. Some models include:</p>
<ul>
<li><p>the Navier-Stokes equations (our most complete understanding of fluid dynamics, except for more advanced models that involve atomic theory; very difficult to solve)</p>
</li>
<li><p>the Euler equations (a simplified model where we neglect friction and heat transfer)</p>
</li>
<li><p>the incompressible Euler equations (a simplified model that neglects fluid compression)</p>
</li>
<li><p>a computer simulation of the incompressible Euler equations (an inaccurate model, because it can only represent discrete timesteps and machine-precision <code>double</code> values instead of real numbers)</p>
</li>
<li><p>my implementation of the simulation of the incompressible Euler equations (which might introduce some bugs)</p>
</li>
</ul>
<p>Another model of reality is of course "intuition", i.e. whatever you're doing when you're looking at a picture of a wave and thinking "that's how the wave will move in the next second".</p>
<p>For all of those models to be useful, they have to agree, i.e. predict the same things (at least to some extent).</p>
<h2>The dam break</h2>
<p>Imagine a wall of water: one meter of water to the right, and nothing to the left. (You can imagine a dam separating the water that suddenly disappeared at t=0). What do the different models tell us will happen?</p>
<p>Your mental model of the world should imagine the water evolving like this:</p>
<p><a class="embed-card" href="https://www.youtube.com/watch?v=i5bpA-f8FW0">https://www.youtube.com/watch?v=i5bpA-f8FW0</a></p>

<p>The incompressible NS equations tell us a similar, but more detailed, story. (The math assumes that you are familiar with partial derivatives and the notation \(\text{div }\mathbf{u} =: \nabla \cdot \mathbf{u}\)).</p>
<ul>
<li><p>The wall of water starts at rest, i.e. \(u = 0\), and evolves according to:</p>
<p>$$\begin{aligned} \frac{d \mathbf{u}}{d t} + \mathbf{u} \cdot \nabla \mathbf{u} &amp;= \mathbf{f} - \frac{\nabla p}{\rho} \ \nabla \cdot \mathbf{u} &amp;= 0 \end{aligned}$$</p>
<p>where</p>
<ul>
<li><p>\(\mathbf{u} \in \mathbb{R}^3\) is the fluid velocity</p>
</li>
<li><p>\(p \in \mathbb{R}\) is the pressure</p>
</li>
<li><p>\(\rho \in \mathbb{R}\) is the density</p>
</li>
<li><p>\(\mathbf{f} \in \mathbb{R}^3\) are the external forces</p>
</li>
</ul>
</li>
<li><p>Nonzero gravity (\(\mathbf{f} = -9.81 \rho \vec{z}\) ), and zero convective transport in the absence of velocity ( \(\mathbf{u} \cdot \nabla \mathbf{u} = 0\)), would cause a downwards velocity everywhere, if there wasn't the pressure term to balance it out.</p>
</li>
<li><p>The pressure corresponds to hydrostatic pressure, i.e. high pressure at the bottom and a low pressure at the top. This pressure exactly compensates the effect from gravity, so that the downwards velocity is zero everywhere.</p>
</li>
<li><p>But pressure is non-directed - so the high pressure at the bottom, or rather the difference between this high pressure and the low air pressure next to it, will cause the fluid to move to the right at the bottom. Similarly, the fluid will move to the left at the top.</p>
</li>
<li><p>Now that fluid is leaking at the bottom, the fluid at the top will also move downwards due to gravity.</p>
</li>
<li><p>The whole wall of water will spectacularly break down.</p>
</li>
</ul>
<p>Here, we have an agreement between intuition, the above video, and the incompressible Navier-Stokes equations. So all of these models are useful!</p>
<h2>Solving the incompressible Euler equations</h2>
<p>The incompressible Euler equations are already a simplified model, but they are still differential equations involving continuous quantities over continuous space evolving in continuous time. To simulate them, we need to discretize them both in time and in space (casting them from a differential equation to a difference equation) and then to solve them.</p>
<p>The incompressible Euler equations are as follows (from <a href="https://en.wikipedia.org/wiki/Euler_equations_(fluid_dynamics)#Incompressible_Euler_equations">Wikipedia</a>):</p>
<p>$$\begin{align} \frac{\partial \rho}{\partial t} + \mathbf{u} \cdot \nabla\rho &amp;= 0 &amp; \text{(a)} \ \frac{\partial \mathbf{u}}{\partial t} + \mathbf{u} \cdot \nabla \mathbf{u} &amp;= - \frac{\nabla p}{\rho} + \mathbf{f} &amp; \text{(b)} \ \nabla \cdot \mathbf{u} &amp;= 0 &amp; \text{(c)} \end{align}$$</p>
<p>In our case, we also have a no-slip boundary condition at the walls: \( \vec{n} \cdot \mathbf{u} |_{\partial \Omega} = 0\).</p>
<p>(a) represents the mass conservation, (b) the impulse conservation, and (c) the incompressibility. (a) is handled by VoF, because VoF is what we use to model mass and density. We will (somewhat arbitrarily) first solve (b) and (c) for the velocity \(\mathbf{u}\), then solve (a) based on that velocity.</p>
<p>(Reference for the previous paragraph: <a href="https://web.stanford.edu/class/me469b/handouts/incompressible.pdf">https://web.stanford.edu/class/me469b/handouts/incompressible.pdf</a>)</p>
<h3>The projection method</h3>
<p>When trying to solve (b), we encounter the problem that \(p\) has no explicit expression, and isn't even conserved between time steps. The solution is to use a projection method. The idea is to solve (b) and (c) at the same time and solve them for both \(\mathbf{u}\) and \(p\).</p>
<p>This section is based on <a href="https://orbi.uliege.be/bitstream/2268/2649/1/BBEC9106.pdf">https://orbi.uliege.be/bitstream/2268/2649/1/BBEC9106.pdf</a>.</p>
<p>The idea behind the projection method was described above: we consider "what would the velocity do if there was no pressure?", i.e. we first compute a "transport velocity" \(\mathbf{u}_\text{trans}\) that solves the first equation without the pressure term. Then we compute what the pressure must be to fulfill both (ii) and (iii).</p>
<p>We partly discretize the equations with an explicit scheme into</p>
<p>$$\begin{align} \frac{\mathbf{u}^{(i+1)} - \mathbf{u}^{(i)}}{\Delta t} + \mathbf{u}^{(i)} \cdot \nabla \mathbf{u}^{(i)} &amp;= - \frac{\nabla p}{\rho} + \mathbf{f} \ \nabla \cdot \mathbf{u}^{(i+1)} &amp;= 0 \end{align}$$</p>
<p>$$\implies \begin{align} \mathbf{u}^{(i+1)} &amp;= \mathbf{u}^{(i)} - \Delta t ( \mathbf{u}^{(i)} \cdot \nabla \mathbf{u}^{(i)} ) - \Delta t \frac{\nabla p}{\rho} + \Delta t \mathbf{f} \ \nabla \cdot \mathbf{u}^{(i+1)} &amp;= 0 \end{align}$$</p>
<p>Define \(\mathbf{u}_\text{trans} := -\mathbf{u}^{(i)} \cdot \nabla \mathbf{u}^{(i)} + \mathbf{f}\)</p>
<p>$$\begin{align} \mathbf{u}^{(i+1)} &amp;= \mathbf{u}^{(i)} + \Delta t \mathbf{u}_ \text{trans} - \Delta t \frac{\nabla p}{\rho} \ \nabla \cdot \mathbf{u}^{(i+1)} &amp;= \nabla \cdot \mathbf{u}^{(i)} = 0 \ \implies \nabla \cdot (\mathbf{u}\text{trans} -\frac{\nabla p}{\rho}) &amp;= 0 \ \implies \nabla \cdot \frac{\nabla p}{\rho} &amp;= \nabla \cdot \mathbf{u}\text{trans} \end{align}$$</p>
<p>where we can compute the right-hand side with finite differences. Then we can solve this differential equation and get \(p\) (up to a constant; we can arbitrarily set the mean pressure to 0).</p>
<p>You might wonder why we solve for \(p\) while we're only interested in \(\frac{\nabla p}{\rho}\). But the equation \(\frac{\nabla p}{\rho} = \mathbf{u}_\text{trans}\) does not have a unique solution for \(\nabla p\); rather, it must be combined with the knowledge that \(\nabla p\) is a gradient (and therefore rotation-free) to get a unique solution. This is what the Poisson-like equation is trying to achieve.</p>
<h4>Algorithm:</h4>
<p>$$\begin{aligned} \mathbf{u}\text{trans} &amp;\leftarrow -( \mathbf{u}^{(i)} \cdot \nabla \mathbf{u}^{(i)} ) + \mathbf{f} \ p &amp;\leftarrow solve\left(\nabla \cdot \frac{\nabla p}{\rho} = \nabla \cdot \mathbf{u}\text{trans}\right) \ \mathbf{u}^{(i+1)} &amp;\leftarrow \mathbf{u}^{(i)} + \Delta t \mathbf{u}_\text{trans} - \Delta t \frac{\nabla p}{\rho} \end{aligned}$$</p>
<p><strong>To compute \(\mathbf{u}_\text{trans}\)</strong>: Use the fact that (using index notation):</p>
<p>$$\mathbf{u} \cdot \nabla \mathbf{u} = u_i \frac{\partial u_j}{\partial x_i} = \frac{\partial u_i}{\partial x_i} u_j + u_i \frac{\partial u_j}{\partial x_i} = \frac{\partial u_i u_j}{\partial x_i} = \nabla \cdot (\mathbf{u} \otimes \mathbf{u})$$</p>
<p>because \(\frac{\partial u_i}{\partial x_i} = \nabla \cdot \mathbf{u} = 0\). So we can compute \(\mathbf{u} \cdot \nabla \mathbf{u}\) by computing \(\mathbf{u} \otimes \mathbf{u}\) for all points and taking derivatives (or finite differences).</p>
<p><strong>To compute \(p\)</strong>: We approximate the differential equations with finite differences. If the density was constant, this would be a Poisson equation; but the density is not constant, so we need a slightly different stencil: (here \(p_i\) denotes "\(p\) at cell \(i\)".)</p>
<p>$$\nabla \cdot \frac{\nabla p}{\rho} \approx \frac{1}{\Delta x^2} (1/\rho_{i+1/2} (p_{i+1} - p_{i}) - 1/\rho_{i-1/2} (p_{i} - p_{i-1}))$$</p>
<p>This corresponds to the stencil</p>
<p>$$\frac{1}{\Delta x^2} \begin{bmatrix} \frac{1}{\rho_{i+1/2}} &amp; (-\frac{1}{\rho_{i+1/2}} - \frac{1}{\rho_{i-1/2}}) &amp; \frac{1}{\rho_{i-1/2}} \end{bmatrix}$$</p>
<p>We then solve this system of finite difference equations with <a href="https://libeigen.gitlab.io/eigen/docs-nightly/classEigen_1_1ConjugateGradient.html">Eigen's Conjugate Gradient solver</a>, which is recommended for the 3D Poisson equation.</p>
<p><strong>Discretization in space</strong>: I discretize</p>
<ul>
<li><p>the pressure on the cell centers; therefore the pressure gradient on the cell faces can be approximated trivially by the pressure difference</p>
</li>
<li><p>\(\rho\) is also given on the cell centers, therefore the \(\rho_{i \pm 1/2}\) above require averaging</p>
</li>
<li><p>\(\mathbf{u}\) on the cell faces. therefore \(\nabla \cdot \mathbf{u}\) can be approximated trivially on the cell centers</p>
</li>
<li><p>\(\mathbf{u} \otimes \mathbf{u}\) on the cell centers by averaging the velocities from the neighboring faces</p>
</li>
<li><p>\(\mathbf{u}_\text{trans}\) on the cell centers; it involves \(\mathbf{f}\), given on the cell centers, and \(\mathbf{u} \otimes \mathbf{u}\)</p>
</li>
</ul>
<p>I think this is the discretization that requires the least effort.</p>
<h2>Volume of Fluid and solving the density equation</h2>
<p>This section is adapted from <a href="https://link.springer.com/article/10.1007/s41745-024-00424-w">Volume of Fluid: A Brief Review</a>.</p>
<p>Volume of Fluid (VoF) tracks the distribution of two fluids in a system. Since the fluids have different, but constant, densities, tracking fluid distributions is equivalent to enforcing the mass conservation from the Navier-Stokes equations:</p>
<p>$$\frac{\partial \rho}{\partial t} + \mathbf{u} \cdot \nabla\rho= 0$$</p>
<p>We specifically use the geometric VoF approach, as opposed to the algebraic VoF approach; geometric VoF is more standard. In geometric VoF, we reconstruct the fluid surface to decide how the fluid evolves.</p>
<p>To reconstruct the fluid interface, we use the piecewise linear interface calculation (PLIC) approach. We look at a 3x3x3 cube of cells and their volume fractions, find the normal vector that best matches the volume fractions in those cells, and set the center cell's normal vector as that vector.</p>
<p>Once we have the normal vector and the volume fraction in a cell, we can compute the "cell wet wall sizes", i.e. the fraction of a the cell wall sizes that is occupied by the fluid. The following image shows in blue the cell wet wall sizes in a given configuration:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6872696ce659b1dad2c6a371/a580ddbc-f6af-49e9-b1ba-5bd6e1d8c63d.png" alt="A graphic with sliders and a unit cube. The unit cube contains a red arrow and its walls are partially blue." style="display:block;margin:0 auto" />

<p>Then, we compute the flux between cells by multiplying the velocity with the cell wet wall size. We should also take into account the fact that these walls have a slope. In 2D, the following figure explains the slope (figure adapted from <a href="https://link.springer.com/article/10.1007/s41745-024-00424-w">Volume of Fluid Method: a Brief Review</a>):</p>
<img src="https://cdn.hashnode.com/uploads/covers/6872696ce659b1dad2c6a371/b29674d4-f492-4096-8b0e-01155d13b27b.png" alt="" style="display:block;margin:0 auto" />

<p>The wet wall size is the blue line on the right; the advected volume is not just the rectangle given by \(\text{wallsize} \cdot u_{i+\frac{1}{2},j} \Delta t\), but rather the trapeze highlighted in blue. A special case happens if the time step is large enough that the blue trapeze meets the top line. In 3D, this is even more complicated. So I just ignored the slope and considered only the rectangle.</p>
<p>Then, we advect the volumes and update the volume fraction of each cell. Note that there is no guarantee that the volume fractions after the advection are still in the interval \([0, 1]\). If we were using an advanced VoF scheme, we'd have such guarantees. But instead we just clamp them to \([0, 1]\) and do not worry too much about the volume loss.</p>
<h1>Various fun implementation details</h1>
<ul>
<li><p>I added Python bindings for the wet-wall-size formula, so that I can unit-test it from Python. This also allows me to visualize wet wall sizes, which is how I generated the plot above.</p>
</li>
<li><p>I added Numpy loading of matrices, instead of hardcoding the initial conditions. For this, I used the <a href="https://github.com/rogersce/cnpy">cnpy</a> library.</p>
</li>
</ul>
<h1>Results</h1>
<p>The simulation results for a dambreak-like scenario look like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6872696ce659b1dad2c6a371/4bf4b412-b334-4c18-9b22-70ccd552fef9.gif" alt="" style="display:block;margin:0 auto" />

<p>The overall shape of the dambreak is as it should, but there is still some non-physical behavior with non-fluid bubbles forming behind the wave. There is another error (possibly linked) where the mesh becomes completely chaotic at the end (the program then usually ends with a failed assertion because some values are <code>NaN</code>).</p>
<p>The main computational bottleneck is the Eigen matrix solver to get the pressure. As far as I know, this is a fundamental limitation of the incompressible Euler equations. In the incompressible mode, the pressure everywhere is coupled and any grid point can influence any other grid point (this is different from the compressible Euler equations, which have only conservation equations and where there is a "finite signal speed").</p>
<h1>Conclusion and future steps</h1>
<p>This post outlined the implementation of a volume-of-fluid (VoF) scheme to simulate the incompressible Euler equations. The behavior of the simulation code mostly follows physics, but still has a few issues. The remaining steps are:</p>
<ul>
<li><p>Fix any remaining VoF bugs</p>
</li>
<li><p>Use a more advanced VoF method with volume conservation guarantees</p>
</li>
<li><p>Implement the compressible Euler equations</p>
</li>
<li><p>Porting the solver to CUDA</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Part 4: 3D meshes and Marching Cubes]]></title><description><![CDATA[Motivation
So far, we’ve simulated featureless, undulating orange grids, which I find boring. What’s really cool would be an animation of a droplet of water falling into a water surface, including all]]></description><link>https://pbhnblog.ballif.eu/3d-meshes-and-marching-cubes</link><guid isPermaLink="true">https://pbhnblog.ballif.eu/3d-meshes-and-marching-cubes</guid><category><![CDATA[computer graphics]]></category><category><![CDATA[3d]]></category><category><![CDATA[C++]]></category><dc:creator><![CDATA[Pierre Ballif]]></dc:creator><pubDate>Sun, 23 Nov 2025 20:38:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763929983654/5e66e0ad-2fc6-40e1-a056-ab7e0239b1ee.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Motivation</h1>
<p>So far, we’ve simulated featureless, undulating orange grids, which I find boring. What’s really cool would be an animation of a droplet of water falling into a water surface, including all the splashing effects, like this:</p>
<img alt="A water droplet. Licensed under Creative Commons by Lóránt Szabó" style="display:block;margin:0 auto" />

<p>A quick search tells me that this is typically done with the <a href="https://en.wikipedia.org/wiki/Volume_of_fluid_method">Volume of Fluids method</a> — roughly, modeling voxels that are filled with fluids to different degrees. My first concern was “doesn’t that completely ignore surface tension?”, but apparently there are approaches to compensate for this (see e.g. <a href="http://www.sciencedirect.com/science/article/pii/S0301932213000190">here</a>).</p>
<p>To get to this result, the first thing we need is to have a 3D grid, instead of a 2D one; or even better, have a template class that can represent any number of dimensions. This is done in commit <a href="https://github.com/Warggr/waves-on-cuda/commit/0c5a35adc23121a9b2789d08e705024a4520ed82">0c5a35a</a>. Then we’ll have to rewrite the GLFW code to display not a 2D grid, but a 3D grid. This is very far from trivial. This post is about how we can do it with the Marching Cubes algorithm.</p>
<h1>Marching Cubes</h1>
<p>Marching Cubes [^2] is an algorithm from computer graphics that, given a scalar field on a rectangular grid, computes a mesh of triangles that approximates an iso-surface. Without loss of generality, we assume that we want to compute the isosurface of all points where the scalar field is zero.</p>
<p>The idea is to look at the spaces between the points of the grid; these spaces form cubes, with 8 mesh points forming the vertices. If the vertices do not all have the same signs, then the iso-surface passes inside the cube (because the iso-surface must separate the positive points from the positive ones. Depending on <em>which</em> vertices have which sign, we select a different pattern of triangles.</p>
<h2>Algorithm</h2>
<p>The original marching cubes [^2] had 14 cases. However, it did not take into account ambiguities, which could cause it to generate holes in the surface [^1]. The updated MC algorithm that we are using goes as follows:</p>
<ol>
<li><p>For a given combination of “on” and “off” vertices, find which of the 14 cases applies.</p>
</li>
<li><p>The case may have multiple sub-cases; we need to run some face tests and/or the interior test to decide which sub-case applies.</p>
</li>
</ol>
<h3>Cases</h3>
<p>There are 2^8 = 256 combinations of “on” and “off” vertices; we reduce them to 14 cases by exploiting rotations (a cube has 23 possible rotations plus the “identity” rotation), and the fact that the “on” and “off” vertices can be swapped (the surface then still separates “on” from “off” vertices - the surface normal has to be inverted though). We do <em>not</em> exploit symmetry; the only case where that would play a role (in other words, the only case that is chiral) is case 11, which we are happy to duplicate into its chiral opposite, case 14.</p>
<h3>Tests</h3>
<p>When a face has two diagonally opposed corners to one side of the isosurface, and the two other corners to the other side, the face test tells you whether the positive corners or the negative corners are connected, like this:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763748392578/7b00fc99-9ec1-4a14-9628-210bd6e0354c.png" alt="" style="display:block;margin:0 auto" />

<p>Similarly, when two diagonally opposed vertices of the cube have the same sign, and there are some vertices with a different sign in between, the center test tells you whether the two vertices are connected or not:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763748475554/cf4a38d1-3540-4eaf-9dc9-b01085651107.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763748497081/ee2f3987-8ca5-43ab-a94f-ee814a2511fa.png" alt="" style="display:block;margin:0 auto" />

<p>Both tests operate under the assumption that the function varies trilinearly inside the cube.</p>
<p>Based on the test results, we then select a sub-case. For example in the two plots above, the positive-negative pattern (0 and 7 positive, the rest negative) tells us that we are in case 4; depending on the result of the center test, we have to select either subcase 4.1 (points not connected) or case 4.2 (points connected).</p>
<h2>Symmetry</h2>
<p>The cases are given up to symmetry, i.e. the combination (vertices 1 and 6 positive, the rest negative) should be turned into (0 and 7 positive, the rest negative), which we recognize as corresponding to case 4. Additionally, the <em>sub-cases</em> are also given up to symmetry.</p>
<p>We’ll distinguish between “original” coordinates, which is how the cube actually is, and the “canonical” coordinates, which is how the current case or subcase is given in the literature. In the example above, (1,6) are the original coordinates of the positive vertices; then we turn the cube to map them into (0,7), which are the canonical positive vertices of case 4. Let’s take the most complicated case, case 13.</p>
<p>We first have to recognize the pattern of positive edges, and turn the cube so that it corresponds to the canonical case 13. Then, we perform 6 face tests and one interior test. If there are more negative face tests than positive ones, we negate the value of each vertex (this also negates the values of the face tests) and rotate the cube to be in the canonical case 13 again, but now with more positive than negative tests.</p>
<p>If none of the tests are positive, we are in case 13.1. If one of them is positive, case 13.2 — but it has to be rotated to match which face is positive. For two positives, we assume that the two positives are on adjacent faces and rotate case 13.3 to fit. (Why are we allowed to assume that the positive faces are adjacent? Because the assumption of tri-linearity means that if there were two positives on opposite faces, then <em>all</em> faces would be positive, not only these two.)</p>
<h3>What to precompute</h3>
<p>We have to decide on a trade-off: what do we precompute and what do we compute at runtime? In other words, where do we use lookup tables and where do we use <code>if</code>-statements? One extreme would be to build a lookup table of 256 × 2^7 (the number of possible test results) entries, where each cell is a list of triangles. The other extreme would be, whenever we encounter a bit-pattern, to try all rotations of it and see whether it matches any canonical case.</p>
<p>My compromise is the following:</p>
<ul>
<li><p>There is a lookup table that maps positive/negative vertex patterns to a rotation + a case (1-14).</p>
</li>
<li><p>We already compute the points where the isosurface intersects the edge (there are 12 edges, but not all of them are crossed by the isosurface). At this point, we have every geometrical information about the cube available as an array. This means that rotations can be implemented as permutations (of both the vertices and edges).</p>
</li>
<li><p>The CPU performs the rotation, then looks up the number of tests for the case and performs all these tests (on the rotated cube).</p>
</li>
<li><p>There is a per-case lookup table that maps test results to a rotation + a subcase.</p>
</li>
<li><p>The CPU performs the rotation again, then draws triangles between the points computed in step 2. We computed the 3D points back when the cube was in “original” coordinates, but the array containing them has been permuted to correspond to the “canonical” list of 3D points for this case. So we can just construct triangles from those points in the order in which they are given, without having to permute things back.</p>
</li>
</ul>
<h1>How to generate cases</h1>
<p>To describe the triangle patterns for each case, I used Python. The data was then translated to C by introspecting the Python data structures, and writing them to a C header; then writing the data as a <code>.c</code> file. The files are then compiled as usual by the compiler.</p>
<p>A nice thing is that I now also have the data structures available in Python. Translating the algorithm from C++ to Python is trivial, which allows me to plot Marching Cubes in Python, and in particular to get an interactive visualization of different cube patterns.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762555339832/36e9796b-3e03-4e5a-a748-82528fb3df9e.png" alt="" style="display:block;margin:0 auto" />

<p>To showcase this, I’ve uploaded the visualization publicly: <a href="https://warggr.github.io/waves-on-cuda/scripts/marching_cubes/html">https://warggr.github.io/waves-on-cuda/scripts/marching_cubes/html</a>/. Thanks to <a href="https://pyodide.org/en/stable/">Pyodide</a>, it is possible to run Python (including Matplotlib and a lot of other third-party modules) in the browser as a static webpage, without needing a server.</p>
<h1>Other implementations</h1>
<p>How does my implementation compare to others?</p>
<p>I added two other Marching Cubes implementations into my CMake project, writing a tiny wrapper so that they are replacements for my <code>marching_cubes</code> function.</p>
<ul>
<li><p>I could not find the source code for the original implementation of Marching Cubes 33 by Lewiner et al. [^3] (they provide a <a href="http://www.acm.org/jgt/papers/LewinerEtAl03">link</a> that is now dead.)</p>
</li>
<li><p>The first implementation is from Custodio et al., based on their 2013 paper [^4]. They fix three issues in the MC33 algorithm.</p>
</li>
<li><p>The second implementation is from Vega et al., the authors of [^1], which was published in 2019. They state that the main contribution of their paper is the correct execution of the interior test, and the efficiency of their implementation.</p>
</li>
</ul>
<h2>Comparison</h2>
<p>Both implementations are <em>much</em> more efficient than mine:</p>
<table>
<thead>
<tr>
<th>Implementation</th>
<th>Runtime [ms]</th>
</tr>
</thead>
<tbody><tr>
<td>Mine</td>
<td>173</td>
</tr>
<tr>
<td>Custodio et al.</td>
<td>59</td>
</tr>
<tr>
<td>Vega et al.</td>
<td>54</td>
</tr>
</tbody></table>
<p>I noticed that both implementations return early when all vertices have the same sign — this is the most common case, so this makes a big difference. After adding an early-return to my code, the runtime decreased to 100ms.</p>
<p>A likely cause for the slowness of my implementation is that I tried to separate the algorithm from the data; each case is processed the same, but based on the case-specific data in the lookup table. The code from the literature (e.g. L. Custodio’s code <a href="https://github.com/liscustodio/modified_mc33/blob/master/source/MarchingCubes.cpp">here</a>) is longer and more branch-heavy. My code was easier (and less boring) to write, but this has a cost in terms of speed.</p>
<h1>Future steps</h1>
<p>First, there are still some bugs in my implementation. For example, I always compute the face test between the bottom-left and top-right vertex of a face, no matter how the cube is rotated; this is of course incorrect.</p>
<p>Second, it might be possible to implement Marching Cubes on the GPU, using a <a href="https://learnopengl.com/Guest-Articles/2022/Compute-Shaders/Introduction">compute shader</a> and/or a <a href="https://learnopengl.com/Advanced-OpenGL/Geometry-Shader">geometry shader</a>. This might make the program faster.</p>
<p>Finally, there are many ways how the Marching Cubes implementation could be sped up. For example, we currently save different sub-cases as lists of triangles, where each triangle is saved as a list of vertices. But the vertices of the triangles are the same for each sub-case of the same case. Maybe vertices could be saved for a case, and then each sub-case could only contain indices indicating how these vertices are connected.</p>
<h1>Conclusion</h1>
<p>The code is now able to visualize 3D grids thanks to a Marching Cubes implementation. Despite the apparent simplicity of Marching Cubes, there are many details to pay attention to. Open-source implementations are much faster and so should be preferred.</p>
<p>Cover image: a sphere plotted with Marching Cubes, more precisely a visualization of the field</p>
<p>$$(x - 5)^2 + (y-5)^2 + (z-5)^2$$</p>
<p>I don’t know why three sides are cut off; I probably made an off-by-one error somewhere.</p>
<h1>References</h1>
<p>[^1]: Vega, D., Abache, J., &amp; Coll, D. (2019). A fast and memory-saving marching cubes 33 implementation with the correct interior test. In <em>Journal of Computer Graphics Techniques</em> 8.3.</p>
<p>[^2]: Lorensen, W. E., &amp; Cline, H. E. (1998). Marching cubes: A high resolution 3D surface construction algorithm. In <em>Seminal graphics: pioneering efforts that shaped the field</em> (pp. 347-353).</p>
<p>[^3] Lewiner, T., Lopes, H., Vieira, A. W., &amp; Tavares, G. (2003). Efficient Implementation of Marching Cubes’ Cases with Topological Guarantees. <em>Journal of Graphics Tools</em>, <em>8</em>(2), 1–15. <a href="https://doi.org/10.1080/10867651.2003.10487582">https://doi.org/10.1080/10867651.2003.10487582</a></p>
<p>[^4] Custodio, L., Etiene, T., Pesco, S., &amp; Silva, C. (2013). Practical considerations on Marching Cubes 33 topological correctness. <em>Comput. Graph.</em> 37, 7 (November, 2013), 840–850. <a href="https://doi.org/10.1016/j.cag.2013.04.004">https://doi.org/10.1016/j.cag.2013.04.004</a></p>
]]></content:encoded></item><item><title><![CDATA[CUDA for Wave Simulation - Part 3: Efficient CUDA]]></title><description><![CDATA[Non-trivial CUDA
The current snippet of code calling CUDA is:
cuda_step<<< 1, 1 >>>

This uses only one CUDA thread, and is probably extremely inefficient. To verify this, let’s add some way to time the program. I could use NVIDIA’s nsys, or just the...]]></description><link>https://pbhnblog.ballif.eu/efficient-cuda</link><guid isPermaLink="true">https://pbhnblog.ballif.eu/efficient-cuda</guid><category><![CDATA[cuda]]></category><category><![CDATA[C++]]></category><category><![CDATA[benchmarking]]></category><dc:creator><![CDATA[Pierre Ballif]]></dc:creator><pubDate>Sun, 31 Aug 2025 14:22:19 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-non-trivial-cuda">Non-trivial CUDA</h1>
<p>The current snippet of code calling CUDA is:</p>
<pre><code class="lang-plaintext">cuda_step&lt;&lt;&lt; 1, 1 &gt;&gt;&gt;
</code></pre>
<p>This uses only one CUDA thread, and is probably extremely inefficient. To verify this, let’s add some way to time the program. I could use NVIDIA’s <code>nsys</code>, or just the Linux <code>time</code> command (or some kind of time counters in the program). In any case, I’ll need a new “run configuration”: run the program for a fixed number of cycles to check its efficiency. I don’t think this justifies a new executable, so let’s make this a program flag. For this, I’ll need to integrate the Boost Program Options library (I could also write program options checking by hand, but Boost is easier, better tested and probably more extensible).</p>
<p>This pulls in an additional dependency to our code, but Boost is already pretty widely installed, and requiring it is rather trivial in CMake. (A future TODO: have a fall-back in case the user does not have Boost installed.)</p>
<p>The new run configuration has the following differences with the original:</p>
<ul>
<li><p>There is no visualization, as it could be a bottleneck (I’m not confident in the current communication between UI and simulation) and I don’t want a window to pop up when I run tests.</p>
</li>
<li><p>The program doesn’t call <code>cudaDeviceSynchronize</code> after each step, as that is no longer necessary and can become a bottleneck. (In fact, it is; <code>./src/waves --perf 1000</code> takes 3ms without <code>cudaDeviceSynchronize</code> and 2080ms with it).</p>
</li>
</ul>
<h2 id="heading-nsys-analysis">NSys analysis</h2>
<p>First, let's run the program with <code>nsys</code>:</p>
<pre><code class="lang-shell">$ nsys profile src/waves --perf 100
$ nsys stats report1.nsys-rep
** CUDA API Summary (cuda_api_sum):

 Time (%)  Total Time (ns)  Num Calls    Avg (ns)      Med (ns)    Min (ns)   Max (ns)    StdDev (ns)            Name         
 --------  ---------------  ---------  ------------  ------------  --------  -----------  ------------  ----------------------
     98.0      128,491,979          2  64,245,989.5  64,245,989.5    72,052  128,419,927  90,755,652.8  cudaMallocManaged     
      1.2        1,553,190        100      15,531.9       5,443.5     5,074      962,247      95,655.4  cudaLaunchKernel
[...more lines left out...]
 ** CUDA GPU Kernel Summary (cuda_gpu_kern_sum):

 Time (%)  Total Time (ns)  Instances  Avg (ns)  Med (ns)  Min (ns)  Max (ns)  StdDev (ns)                                                Name                                              
 --------  ---------------  ---------  --------  --------  --------  --------  -----------  ------------------------------------------------------------------------------------------------
    100.0        1,923,059        100  19,230.6  16,096.0    16,063   330,014     31,392.3  cuda_step(const double *, double *, double, double, unsigned long, unsigned long, unsigned long)
</code></pre>
<p>Okay, so the allocations are still too big in comparison with the actual work: we spend 128ms (128,491,979ns) allocating memory, 1.55 ms launching the kernel, and 1.92 ms doing the kernel work. Notes:</p>
<ul>
<li><p>This is particularly bad because as of <code>265a7e2</code>, the <code>--perf</code> configuration doesn't measure the initialization time, incl. memory allocation — so our results are going to be wildly different from the actual runtime if the allocation runtime is significant. (I should probably fix this.)</p>
</li>
<li><p>I don't know if the 1.55ms and 1.92ms should be added or subtracted from each other (i.e. whether kernel launch time counts as part of kernel execution time), but it doesn't really matter for this example.</p>
</li>
<li><p>Sanity check: <code>nsys</code> confirms that we did 2 allocations and launched 100 kernels, as expected. (We allocate one "front" and one "back" matrix buffers and swap them back and forth.)</p>
</li>
</ul>
<p>Let's increase N. I won't copy the output of <code>nsys</code> again, instead here's a summary. All times are in ms.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>N</td><td>10 (previous)</td><td>1000</td></tr>
</thead>
<tbody>
<tr>
<td><code>cudaMallocManaged</code> total time</td><td>128.49</td><td>126.56</td></tr>
<tr>
<td><code>cudaMallocManaged</code> calls</td><td>2</td><td>2</td></tr>
<tr>
<td><code>cudaLaunchKernel</code> total time</td><td>1.55</td><td>127.88</td></tr>
<tr>
<td><code>cudaLaunchKernel</code> number of calls</td><td>100</td><td>10000</td></tr>
<tr>
<td>Kernel run time</td><td>1.92</td><td>161.19</td></tr>
</tbody>
</table>
</div><h2 id="heading-runtime-analysis">Runtime analysis</h2>
<p>Now we can do a few tests with the <code>--perf</code> configuration. All times are in ms.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>N</td><td>10</td><td>100</td><td>1000</td><td>10000</td></tr>
</thead>
<tbody>
<tr>
<td>Without CUDA</td><td>0.07</td><td>0.50</td><td>5.04</td><td>50.08</td></tr>
<tr>
<td>With CUDA</td><td>0.41</td><td>0.27</td><td>125.95</td><td>11314.5</td></tr>
</tbody>
</table>
</div><p>Again, results for \(N \le 10000\) are not really significant in CUDA, because the runtime is dominated by <code>malloc</code>, outside the region tracked by <code>perf</code>. The no-CUDA version scales linearly with N, as expected.</p>
<p>The CUDA version decreases in runtime with higher N, then scales very badly with large N. A possibly related bug is that I forgot to sync the CUDA state after all steps were done. Could this be the explanation?</p>
<p>A <a target="_blank" href="https://medium.com/@snshyam/cuda-deep-dive-what-happens-when-you-launch-a-kernel-034e23624932">nice post on CUDA kernel launching</a> explains the process of launching a kernel, including this interesting detail:</p>
<blockquote>
<p>The driver doesn’t immediately send your kernel to the GPU. Instead, it [...] allocates space in a command buffer — think of this as a todo list for the GPU</p>
</blockquote>
<p>How large is this command buffer? Unfortunately, this is difficult to find out. So for all I know, the following explanation is reasonable: we don't sync the CUDA state and launch a lot of kernels without waiting, so the first 100 or so land in the command buffer. We don't sync, so we measure only the time for launching the kernel, which is tiny. When we add more kernels, at some point the command buffer becomes full and we have to wait for work to be processed. So for higher N's, we effectively add a synchronization that was not present for lower N's, which makes the runtime much higher.</p>
<p>In any case, the fix is trivial: Call <code>cudaDeviceSynchronize</code> at the end of each experiment. (See commit <a target="_blank" href="github.com/warggr/waves-on-cuda/commit/cae55ce"><code>cae55ce</code></a>.) With this, we have the following results:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>N</td><td>10</td><td>100</td><td>1000</td><td>10000</td></tr>
</thead>
<tbody>
<tr>
<td>Without CUDA</td><td>0.07</td><td>0.50</td><td>5.04</td><td>50.08</td></tr>
<tr>
<td>With CUDA</td><td>15.20</td><td>140.90</td><td>1128.67</td><td>11285.2</td></tr>
</tbody>
</table>
</div><p>This is more sensible, although the performance is terrible. So now that we've ironed out all the bugs, we can do what we wanted to do in the first place: add more CUDA threads to actually take advantage of the GPU.</p>
<h2 id="heading-increasing-threads-and-grid-size">Increasing threads and grid size</h2>
<p>Using more threads can be done by simply calling the kernel as</p>
<pre><code class="lang-plaintext">cuda_step&lt;&lt;&lt; 1, NUMBER_OF_THREADS &gt;&gt;&gt;
</code></pre>
<p>Then the kernel will be called <code>NUMBER_OF_THREADS</code> times, with a different value of <code>threadId</code> to give each thread an identity. We need to decide what each thread does. Fortunately, in our simple problem, each row of the grid is independent, so we can just give one or more rows to each thread. This is done with the following code:</p>
<pre><code class="lang-diff"> void cuda_step(
     const double* in, PlainCGrid out,
     double t, double c,
<span class="hljs-addition">+    std::size_t block_size,</span>
     std::size_t grid_width, std::size_t grid_height
 ) {
<span class="hljs-addition">+    int start = threadIdx.x * block_size;</span>
<span class="hljs-addition">+    int end = (threadIdx.x + 1) * block_size;</span>
<span class="hljs-deletion">-    for(int i = 0; i &lt; grid_height; i++) {</span>
<span class="hljs-addition">+    for(int i = start; i &lt; end; i++) {</span>
</code></pre>
<p>where the caller has to specify the block size and to ensure that <code>NUMBER_OF_THREADS * block_size == grid_height</code>. In the documentation, it seems allowed to use an arbitrary number of threads, e.g. specify <code>NUMBER_OF_THREADS = grid_height</code> and <code>block_size = 1</code>. In fact, this works on my laptop even for <code>NUMBER_OF_THREADS = 10000</code>, even though the <a target="_blank" href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/#thread-hierarchy">Programming Guide</a> states that</p>
<blockquote>
<p>On current GPUs, a thread block may contain up to 1024 threads.</p>
</blockquote>
<p>This limit can also be confirmed on my GPU with <a target="_blank" href="https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#recommended-post%5B/url%5D"><code>deviceQuery</code></a>. I assume that the additional threads are run sequentially on different "physical" threads.</p>
<p>A few experiments with the grid size. I use different values of \(S\), the number of elements per side; so there are \(S^2\) elements on the full grid. The time step \(dt\) is adapted to fulfill the CFL condition, but the number of time steps stays constant, namely N=100000 timesteps. The number of time steps has to be chosen sufficiently large, otherwise <code>cudaDeviceSynchronize</code> takes up most of the runtime (since it now has to sync \(O(S^2)\) memory, which can be very large).</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>S</td><td>10</td><td>50</td><td>100</td><td>500</td><td>1000</td></tr>
</thead>
<tbody>
<tr>
<td>Without CUDA</td><td>7.67</td><td>112.70</td><td>495.12</td><td>154523</td><td></td></tr>
<tr>
<td>With CUDA, 1 thread</td><td>1697.91</td><td>29064.3</td><td>111808</td><td>3902360</td><td></td></tr>
<tr>
<td>With CUDA, S threads</td><td>372.36</td><td>1125.84</td><td>2002.39</td><td>41444</td><td>210764</td></tr>
</tbody>
</table>
</div><p>In theory, the work scales with the number of elements, i.e. with \(S^2\). We observe the following:</p>
<ul>
<li><p>The non-CUDA version scales worse than \(O(S^2)\). This could be due to cache effects (for S = 500, each grid takes <code>500*500*sizeof(double)</code> i.e. 2MB; on my machine, this fits into L3 cache but not into L2.)</p>
</li>
<li><p>The CUDA 1-thread has worse performance than the non-CUDA version in the beginning, but scales like \(S^2\), i.e. slightly better. This might be due to different cache effects.</p>
</li>
<li><p>The CUDA S-thread version scales like \(S^2\) for high values of \(S^2\). I would have expected it to scale like \(S\): Each thread takes one column (\(S\) elements), and there should be enough hardware threads so that no columns have to be processed sequentially.</p>
</li>
</ul>
<h2 id="heading-merging-kernels">Merging kernels</h2>
<p>As seen previously, launching a kernel is a somewhat complex process. Can we put the for-loop over N in the kernel, so that we only launch the kernel once? Each thread's work is independent, so there should be no synchronization issues. This is implemented in <a target="_blank" href="github.com/warggr/waves-on-cuda/commit/6bd0ee7"><code>6bd0ee7</code></a>. The runtime is as follows:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>S</td><td>10</td><td>50</td><td>100</td><td>500</td><td>1000</td></tr>
</thead>
<tbody>
<tr>
<td>Without CUDA</td><td>7.67</td><td>112.70</td><td>495.12</td><td>154523</td><td></td></tr>
<tr>
<td>With CUDA, 1 thread</td><td>1697.91</td><td>29064.3</td><td>111808</td><td>3902360</td><td></td></tr>
<tr>
<td>With CUDA, S threads</td><td>372.36</td><td>1125.84</td><td>2002.39</td><td>41444</td><td>210764</td></tr>
<tr>
<td><strong>With CUDA, S threads, one kernel</strong></td><td>178.77</td><td>661.35</td><td>1848.84</td><td>41206</td><td>210291</td></tr>
</tbody>
</table>
</div><p>So this is an improvement, but becomes less relevant for high \(S\), as the actual kernel execution and memory transfer will be more prevalent than the kernel launching overhead.</p>
<h1 id="heading-future-work">Future work</h1>
<h2 id="heading-bugs">Bugs</h2>
<ul>
<li><p>Even though the <code>--perf</code> version runs fine, there's now a SEGFAULT when running without <code>--perf</code>, i.e. with the GUI.</p>
</li>
<li><p>CUDA with \(S = 10^4\) threads and \(N = 1000\) reports 0.64 ms runtime, where \(S=10^3\) took 2162ms. There is probably something somewhere that immediately fails and exits.</p>
</li>
</ul>
<h2 id="heading-features">Features</h2>
<ul>
<li><p>I never actually checked whether the program transformations were correct, i.e. whether the result is the same for every commit so far.</p>
</li>
<li><p>Use thread groups, i.e. put a non-1 number in the syntax</p>
</li>
</ul>
<pre><code class="lang-plaintext">cuda_step&lt;&lt;&lt; 1, NUMBER_OF_THREADS &gt;&gt;&gt;
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Part 2: Basic CUDA]]></title><description><![CDATA[There’s a good first CUDA tutorial here: https://developer.nvidia.com/blog/even-easier-introduction-cuda/. Then there are the docs here: https://docs.nvidia.com/cuda/cuda-c-programming-guide/#.
The docs describe three levels of the thread hierarchy —...]]></description><link>https://pbhnblog.ballif.eu/basic-cuda</link><guid isPermaLink="true">https://pbhnblog.ballif.eu/basic-cuda</guid><category><![CDATA[cuda]]></category><category><![CDATA[simulation]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[C++]]></category><dc:creator><![CDATA[Pierre Ballif]]></dc:creator><pubDate>Sun, 17 Aug 2025 19:14:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763930516165/4e2210cf-3576-43a6-bbfb-a28ff49c5b16.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There’s a good first CUDA tutorial here: <a target="_blank" href="https://developer.nvidia.com/blog/even-easier-introduction-cuda/">https://developer.nvidia.com/blog/even-easier-introduction-cuda/</a>. Then there are the docs here: <a target="_blank" href="https://docs.nvidia.com/cuda/cuda-c-programming-guide/#">https://docs.nvidia.com/cuda/cuda-c-programming-guide/#</a>.</p>
<p>The docs describe three levels of the thread hierarchy — thread blocks, thread block clusters, and grids. We will use only one thread block, because it seems easiest — in particular, sharing memory outside thread blocks seems difficult. This will limit the level of parallelism we’ll be able to achieve; a future version could try to use multiple thread blocks.</p>
<p>The current simulation code ( World::step ) looks like this:</p>
<pre><code class="lang-c++"><span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; GRID_HEIGHT; i++) {
    (*other_grid)[i][<span class="hljs-number">0</span>] = <span class="hljs-number">1.0</span>;
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> j = <span class="hljs-number">1</span>; j&lt;GRID_WIDTH; j++) {
        (*other_grid)[i][j] = (*current_grid)[i][j<span class="hljs-number">-1</span>];
    }
}
</code></pre>
<p>This can be easily converted to a CUDA kernel:</p>
<pre><code class="lang-c++"><span class="hljs-keyword">using</span> PlainCGrid = <span class="hljs-keyword">double</span>*;

<span class="hljs-function">__global__
<span class="hljs-keyword">void</span> <span class="hljs-title">cuda_step</span><span class="hljs-params">(PlainCGrid in, PlainCGrid out)</span> </span>{
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; GRID_HEIGHT; i++) {
        out[i*GRID_WIDTH] = <span class="hljs-number">1.0</span>;
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> j = <span class="hljs-number">1</span>; j&lt;GRID_WIDTH; j++) {
            out[i*GRID_WIDTH + j] = in[i*GRID_WIDTH + j<span class="hljs-number">-1</span>];
        }
    }
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">World::step</span><span class="hljs-params">()</span> </span>{
    cuda_step&lt;&lt;&lt;<span class="hljs-number">1</span>, <span class="hljs-number">1</span>&gt;&gt;&gt;(
        <span class="hljs-keyword">reinterpret_cast</span>&lt;<span class="hljs-keyword">double</span>*&gt;(current_grid-&gt;_data.data()),
        <span class="hljs-keyword">reinterpret_cast</span>&lt;<span class="hljs-keyword">double</span>*&gt;(other_grid-&gt;_data.data())
    );
    <span class="hljs-built_in">std</span>::swap(other_grid, current_grid);
}
</code></pre>
<p>Now we’ll have to compile it, ideally not manually but by integrating it into the CMake project. A CUDA/CMake tutorial can be found here: <a target="_blank" href="https://developer.nvidia.com/blog/building-cuda-applications-cmake/">https://developer.nvidia.com/blog/building-cuda-applications-cmake/</a></p>
<p>This is where I notice it doesn’t work; I just get some garbled triangles. The error is not with the C-style rewrite, as removing the <code>__global__</code> and the <code>&lt;&lt;&lt;1,1&gt;&gt;&gt;</code> works perfectly (see commit <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/commit/1b6d3b9">1b6d3b9d</a>). Probably I am doing something wrong with CUDA memory management.</p>
<p>One problem is that we were using <code>std::array</code>s , but now we need CUDA-managed memory, not “regular” memory. CUDA can’t interact with the “regular” memory stored in the <code>std::array</code>. All of the following do not work:</p>
<ul>
<li><p>most C++ containers take as a template parameter an “allocator” which can allocate memory in non-standard ways. However std::array doesn’t (probably because it doesn’t actually allocate memory on the heap, but is just a wrapper around stack memory)</p>
</li>
<li><p>A <code>std::array</code> also cannot be constructed by providing the memory (probably for the same reason).</p>
</li>
<li><p>I could use an <code>std::vector</code>, but I want the size of the grid not to change after initialization, so using a resizable vector seems inelegant to me.</p>
</li>
<li><p>In C++23, I could use <code>std::span</code>, but I’m still targeting C++17</p>
</li>
<li><p>I could write some custom class that is an array with managed memory, but I already have <code>Grid</code> that provides most of that functionality. So I’ll adapt the <code>Grid</code> class.</p>
</li>
</ul>
<p>After adapting <code>Grid</code>, there’s another problem. The program flow (in <code>main</code>) looks like this: some memory is dynamically allocated by Grid, GLFW accesses that memory, then Grid is deleted before GLFW, the memory is freed, and we get a segfault. The short-term solution is to initialize Grid before GLFW. The long-term solution would be to use a smart pointer, or to use Rust, where that kind of thing can’t happen ;)</p>
<p>By <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/commit/c3173dde8b05d6e6eee7fcf26fb599d0e15a5085">c3173dd</a>, after another trivial bug fix, this works and there’s again an unit-height wave coming from the right side. So everything works as it did before, but with CUDA. (Something that was helpful for the trivial bug fix: I can set the <code>GRID_WIDTH</code> and <code>GRID_HEIGHT</code> constant to 10 each and then print the grid to see what happens.)</p>
<p>Now I would like to have some animation in the visualization. The first step is to replace the <code>for</code>-loop in main (that only does 20 timesteps) with a <code>while</code> loop that runs forever. However, that breaks the program flow. The previous program flow was:</p>
<ol>
<li><p>20 simulation iterations are performed and visualized at the same time.</p>
</li>
<li><p><code>main</code> exits and waits for the destructor of <code>MyGLFW</code>, i.e. the visualization, which waits for the UI thread to exit.</p>
</li>
<li><p>When the user presses Exit or closes the window, the UI thread exits and the program stops.</p>
</li>
</ol>
<p>Now we need a more complex flow, which can handle both the user closing the UI thread, the main thread getting an interrupt signal, or the main thread exiting for any other reason. My solution looks like this:</p>
<ul>
<li><p>The UI (<code>class MyGLFW</code>) has a state that can be <code>RUNNING</code> or <code>KILLED</code> (i.e. the user wants to close the window but it hasn’t been closed yet). The window is started in the constructor and closed in the destructor, so there’s no other state (if the <code>MyGLFW</code> exists, then it has a GLFW window somewhere).</p>
</li>
<li><p>The state can be accessed by both threads, and is protected by a mutex. When the UI thread wants to exit, it sets the state to <code>KILLED</code> . When the main thread calls the destructor, the state is also set to <code>KILLED</code>.</p>
</li>
<li><p>On the other side, the UI thread regularly checks the state. If it’s killed, it exits the render loop. Whenever the main loop tries to render something and the UI has been killed, this raises an exception (which is caught but breaks the main loop).</p>
</li>
</ul>
<p>This is done by commit <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/commit/bde64e4">bde64e4</a>. There seems still to be a bug with some huge triangles in the rendering; I’ll debug that later.</p>
<p>I notice that the <code>MyGLFW</code> class does a bit too much and has essentially two separate responsibilities:</p>
<ul>
<li><p>Wrap the GLFW state and ensure the window is properly closed in the destructor</p>
</li>
<li><p>Provide synchronization between the main loop and the UI window</p>
</li>
</ul>
<p>To have a clearer (and thereby probably more robust) program, I’ve split the class into two classes (<code>MyGLFW</code> and <code>renderer</code>). This is done in <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/commit/a8a27e3">a8a27e3</a>.</p>
<p>Now that the UI and simulation can run in parallel, let’s add some more complicated boundary conditions, such as a sine. Some considerations to take into account:</p>
<ul>
<li><p>We can vary the wave speed \(v\) and the sine period. To view an interesting sine, I need a high sine period, a high grid size, and a low wave speed. Probably I’ll need to take into account the <a target="_blank" href="https://en.wikipedia.org/wiki/Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition">CFL condition</a> later.</p>
</li>
<li><p>For debugging, I can set a small grid size, replace the while loop back with a for loop, and print the grid for the first few iterations. Prior to printing the grid (which is in GPU memory but will be mostly copied on the CPU), the program needs to call <code>cudaDeviceSynchronize</code>. (I found that out through this <a target="_blank" href="https://www.irisa.fr/alf/downloads/collange/cours/hpca2020_gpu_2.pdf">doc</a>, which wisely states “We all make this mistake once".)</p>
</li>
<li><p>I can also add an option to not use CUDA, to can distinguish between CUDA usage errors and logic errors (see commit <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/commit/2cb856f24ad020c0336382cdd09318a47b4c9252">2cb856f</a>)</p>
</li>
</ul>
<p>With <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/commit/49f7293">49f7293</a>, the grid size also becomes variable — this goes against what I said previously about having “the container implicitly contain its size”, but it’s a pretty convenient feature. (Since that is no longer a reason, could we use an <code>std</code> container? No — there’s no 2D vector in the STD. There’s a vector of vectors, but it wouldn’t be in continuous memory. So our solution is still the most elegant.)</p>
<p>With <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/commit/f356508">f356508</a>, the boundary condition becomes a sine wave, so we can see the simulation actually doing something.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755457917917/4dc45729-15a9-4dee-b99a-7d35e9e6cda9.webp" alt="A mesh of orange triangles over a black background, forming a wavy sheet." class="image--center mx-auto" /></p>
<p>This brings us to the remaining tasks / directions to continue the program:</p>
<ul>
<li><p>Fix the OpenGL bugs (rectangle getting outside the screen all the time, too large triangles, etc.)</p>
</li>
<li><p>Flesh out the simulation: use different schemes, different equations, different grids, and different boundary conditions</p>
</li>
<li><p>Learn more about CUDA by adding more “difficult” parallelization, e.g. the grids and thread blocks clusters mentioned in the intro. Right now we only use one CUDA thread (AFAIK). This is sufficient, but the goal of this project is to learn about CUDA, so we should use something more complex than that. We can artificially create the need for more complex structures by using more complex grids (e.g. split up the grid into different domains).</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Part 1: OpenGL]]></title><description><![CDATA[Motivation
In this series of posts, I'll try to simulate waves on a GPU using CUDA.
First, I'll describe the exact scope of the project. The goal is to implement some computational fluid dynamics method to solve some fluid dynamics problem - for now,...]]></description><link>https://pbhnblog.ballif.eu/opengl</link><guid isPermaLink="true">https://pbhnblog.ballif.eu/opengl</guid><category><![CDATA[C++]]></category><category><![CDATA[openGL]]></category><category><![CDATA[simulation]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Pierre Ballif]]></dc:creator><pubDate>Sun, 17 Aug 2025 18:56:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763930426168/cf7eb581-74c0-4830-9523-1a7271742e78.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-motivation">Motivation</h1>
<p>In this series of posts, I'll try to simulate waves on a GPU using CUDA.</p>
<p>First, I'll describe the exact scope of the project. The goal is to implement some computational fluid dynamics method to solve some fluid dynamics problem - for now, let's take the upstream finite-difference scheme for the advection equation, because it's one of the easiest setups I can think of. The method will be implemented on the GPU using CUDA. There should also be some sort of visualization to ensure that the results are sensible, but it can be rather basic.</p>
<p>One thing that is not a goal is rendering photo-realistic waves. I know that this is possible on a GPU, and I might look into it more once this project is finished, but for now this is outside my scope.</p>
<p>As a first guess, the steps of the implementation will be as follows:</p>
<ul>
<li><p>basic project setup with a C++ main function, CMake, defining the data structures, and a very basic simulation loop.</p>
</li>
<li><p>getting some visualization of the results</p>
</li>
<li><p>find some numerical method and how to implement it in CUDA</p>
</li>
<li><p>couple the numerical solver in CUDA with the rest of the project</p>
</li>
<li><p>implement more difficult methods, equations, and boundary conditions (possibly using config files)</p>
</li>
</ul>
<p>Once I actually started the project, the first step turned out to be very straightforward, while the second step turned out to be much more difficult than I thought. However, it was also very instructive - I effectively learned the basics of OpenGL. So this first post is going to focus on the first two steps and is essentially an introduction to OpenGL. Once this is done, I will focus on the actual "waves on CUDA" part and write the results in a second post.</p>
<p>The code is version-controlled (of course) and publicly available on my GitHub: <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/">https://github.com/Warggr/waves-on-cuda/</a> . A lot of content is copied and commented here, but often (more often in the later parts) I will just write here a summary of what I did and why. If you want to learn more, I will point you to my repository and some tutorials that I (mostly) followed.</p>
<h1 id="heading-basic-infrastructure-without-cuda">Basic infrastructure without CUDA</h1>
<p>I define the minimum viable product as follows:</p>
<ul>
<li><p>C++ code to define a 2D surface on which waves can travel</p>
</li>
<li><p>some wave movement</p>
</li>
<li><p>wave visualization</p>
</li>
</ul>
<p>The first two should be pretty straightforward. The second will require a front-end library. I'll use <a target="_blank" href="https://www.glfw.org/">GLFW</a> as I've seen it used in a similar project in a lecture; there are probably some better choices. GLFW is based on OpenGL, which is a cross-manufacturer graphics rendering interface. You might say "why use OpenGL if this project is going to be specific to CUDA/NVIDIA?".  I could actually use (some CUDA-specific GL) for graphics rendering as well, but for now I'll use CUDA only for the fluid dynamics part.</p>
<h2 id="heading-basic-c-code">Basic C++ code</h2>
<p>Let's jump into the C++ code. Here's a simple grid with 100x100 <code>double</code> cells, all initialized to 0 (put this into a file called <code>src/grid.hpp</code>):</p>
<pre><code class="lang-c++"><span class="hljs-meta">#<span class="hljs-meta-keyword">pragma</span> once</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;array&gt;</span></span>

<span class="hljs-keyword">constexpr</span> <span class="hljs-keyword">int</span> GRID_WIDTH = <span class="hljs-number">100</span>,
    GRID_HEIGHT = <span class="hljs-number">100</span>;

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Grid</span> {</span>
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">array</span>&lt;<span class="hljs-built_in">std</span>::<span class="hljs-built_in">array</span>&lt;<span class="hljs-keyword">double</span>, GRID_WIDTH&gt;, GRID_HEIGHT&gt; _data;
    Grid() {
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">auto</span>&amp; row: _data) {
            <span class="hljs-keyword">for</span>(<span class="hljs-keyword">double</span>&amp; cell: row) {
                cell = <span class="hljs-number">0</span>;
            }
        }
    }
};
</code></pre>
<p>During simulation, we're going to compute the state at time step n+1 based on the state at time step n - so we need to store both. For now I implement a super simple time scheme where the fluid travels exactly once cell each time step, and the boundary condition is always 1 (i.e. we'll have a wave of 1 going from the left to the right, with the grid being 0 outside the wave).</p>
<pre><code class="lang-c++"><span class="hljs-meta">#<span class="hljs-meta-keyword">pragma</span> once</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;array&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;utility&gt;</span></span>

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Grid</span> {</span>
  ...
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">World</span> {</span>
    Grid grid1, grid2;
    Grid* current_grid, * other_grid;
<span class="hljs-keyword">public</span>:
    World() {
        current_grid = &amp;grid1;
        other_grid = &amp;grid2;
    }
    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">step</span><span class="hljs-params">()</span> </span>{
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; GRID_HEIGHT; i++) {
            (*other_grid)[i][<span class="hljs-number">0</span>] = <span class="hljs-number">1.0</span>;
            <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> j = <span class="hljs-number">1</span>; j&lt;GRID_WIDTH; j++) {
                (*other_grid)[i][j] = (*current_grid)[i][j<span class="hljs-number">-1</span>];
            }
        }
        <span class="hljs-built_in">std</span>::swap(other_grid, current_grid);
    }
};
</code></pre>
<p>To iterate over <code>Grid</code> objects as the code does, we'll need to implement a few methods on the <code>Grid</code> (the <code>const</code> version of the iterators are going to be useful later):</p>
<pre><code class="lang-c++"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Grid</span> {</span>
    <span class="hljs-keyword">using</span> GridArray = <span class="hljs-built_in">std</span>::<span class="hljs-built_in">array</span>&lt;<span class="hljs-built_in">std</span>::<span class="hljs-built_in">array</span>&lt;<span class="hljs-keyword">double</span>, GRID_WIDTH&gt;, GRID_HEIGHT&gt;;
    GridArray _data;
    Grid() {
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">auto</span>&amp; row: _data) {
            <span class="hljs-keyword">for</span>(<span class="hljs-keyword">double</span>&amp; cell: row) {
                cell = <span class="hljs-number">0</span>;
            }
        }
    }
    <span class="hljs-function">GridArray::iterator <span class="hljs-title">begin</span><span class="hljs-params">()</span> </span>{ <span class="hljs-keyword">return</span> _data.begin(); }
    <span class="hljs-function">GridArray::iterator <span class="hljs-title">end</span><span class="hljs-params">()</span> </span>{ <span class="hljs-keyword">return</span> _data.end(); }
    <span class="hljs-function">GridArray::const_iterator <span class="hljs-title">begin</span><span class="hljs-params">()</span> <span class="hljs-keyword">const</span> </span>{ <span class="hljs-keyword">return</span> _data.begin(); }
    <span class="hljs-function">GridArray::const_iterator <span class="hljs-title">end</span><span class="hljs-params">()</span> <span class="hljs-keyword">const</span> </span>{ <span class="hljs-keyword">return</span> _data.end(); }
    <span class="hljs-function"><span class="hljs-built_in">std</span>::<span class="hljs-keyword">size_t</span> <span class="hljs-title">size</span><span class="hljs-params">()</span> </span>{ <span class="hljs-keyword">return</span> _data.size(); }
    <span class="hljs-built_in">std</span>::<span class="hljs-built_in">array</span>&lt;<span class="hljs-keyword">double</span>, GRID_WIDTH&gt;&amp; <span class="hljs-keyword">operator</span>[] (<span class="hljs-keyword">int</span> i) { <span class="hljs-keyword">return</span> _data[i]; }
};
</code></pre>
<p>(at first I forgot to return a reference (<code>&amp;</code>) in the <code>operator[]</code>  - so it didn't work because it always modified a copy of the grid - make sure that doesn't happen to you :) ) Now let's create these files: <code>src/main.cpp</code></p>
<pre><code class="lang-c++"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"grid.hpp"</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span> </span>{
    World world;
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> t = <span class="hljs-number">0</span>; t&lt;<span class="hljs-number">50</span>; t++) {
        world.step();
    }
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">const</span> <span class="hljs-keyword">auto</span>&amp; row: world.grid()) {
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">const</span> <span class="hljs-keyword">auto</span>&amp; cell: row) {
            <span class="hljs-keyword">if</span>(cell &gt; <span class="hljs-number">0</span>) <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; cell;
            <span class="hljs-keyword">else</span> <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-string">"  "</span>;
        }
        <span class="hljs-built_in">std</span>::<span class="hljs-built_in">cout</span> &lt;&lt; <span class="hljs-built_in">std</span>::<span class="hljs-built_in">endl</span>;
    }
}
</code></pre>
<p>I then compile the project using CMake (I'll leave out the steps because all of it is boilerplate) and get, as expected, a front of 1 's advancing from the right. The program is now good enough to be committed (<a target="_blank" href="https://github.com/Warggr/waves-on-cuda/commit/91a068a9497b379857a6b63f30011db20733498e">commit</a>).</p>
<h2 id="heading-integrating-glfw">Integrating GLFW</h2>
<p>I have GLFW installed, so I'll use the local version. A later TODO would be to fetch it if not already installed.</p>
<p><code>CMakeLists.txt</code>:</p>
<pre><code class="lang-plaintext">cmake_minimum_required(VERSION 3.0.0)
project(waves_on_cuda VERSION 0.1.0 LANGUAGES CXX)

find_package(glfw3 3.4 REQUIRED)

add_subdirectory(src)
</code></pre>
<p><code>src/CMakeLists.txt</code>:</p>
<pre><code class="lang-plaintext">add_executable(waves main.cpp)
target_link_libraries(waves glfw)
</code></pre>
<p>GLFW expects to be in full control of the program - you need to write your main loop as <code>while(!glfwWindowShouldClose(window))</code> and call <code>glfwPollEvents()</code> regularly. However, I would find it cleaner to have GLFW <em>not</em> be in control of the program - the main loop should be the wave simulation, and then we pass the results to the rendering after each time step.</p>
<p>I think the cleanest solution is to run GLFW in a separate thread, the UI thread. We'll have a one-way channel of communication, a queue where the main thread posts events whenever a new simulation timestep becomes available, and a final event if the interrupt signal has been received. In the future, we might make the communication more complex to e.g. skip simulating time steps if the renderer is too slow. Having this setup has multiple advantages:</p>
<ul>
<li><p>we decouple GLFW from the main program logic; if we want later to switch to another rendering (or have the option to choose rendering at runtime), this will be easy to refactor</p>
</li>
<li><p>we make no assumptions on whether rendering or simulation are faster; and we can make both as fast as possible (either the rendering will render multiple times the same simulation timestep, or the simulation will simulate timesteps that won't be able to be rendered).</p>
</li>
</ul>
<p>Making GLFW work actually took a couple of hours as there were some bugs to fix. It turns out that GLFW keeps some thread-internal state, and you can't just initialize it in one thread and make it do something in another thread. Furthermore, you typically need an extension loader, such as Glad or GLEW (I used GLEW), which is also not quite straightforward to integrate. Finally, I wanted my assets to be compiled, instead of pasting OpenGL domain specific language as strings into the C++ program. Some more details on this in the following section.</p>
<p>Some links that are generally useful as introductions into OpenGL: <a target="_blank" href="https://antongerdelan.net/opengl/hellotriangle.html">Anton's OpenGL4 tutorials</a><a target="_blank" href="https://www.glfw.org/docs/latest/quick.html">The GLFW quickstart guide</a></p>
<h2 id="heading-compiled-spir-v-shaders">Compiled (SPIR-V) shaders</h2>
<p>OpenGL uses so-called shaders for basically everything (perspective projection, transformation, coloring). They are essentially small functions that are loaded into the GPU and executed there. Shaders are written in a C-like language called GLSL. The easiest way of loading a shader looks like this:</p>
<pre><code class="lang-c++"><span class="hljs-keyword">const</span> <span class="hljs-keyword">char</span>* fragment_shader =
<span class="hljs-string">"#version 410 core\n"</span>
<span class="hljs-string">"out vec4 frag_colour;"</span>
<span class="hljs-string">"void main() {"</span>
<span class="hljs-string">"  frag_colour = vec4( 0.5, 0.0, 0.5, 1.0 );"</span>
<span class="hljs-string">"}"</span>;
GLuint vs = glCreateShader( GL_VERTEX_SHADER );
glShaderSource( vs, <span class="hljs-number">1</span>, &amp;vertex_shader, <span class="hljs-literal">NULL</span> );
glCompileShader( vs );
</code></pre>
<p>I find this incredibly ugly for two reasons. First, you're pasting code as a string in a C program, so you miss all the benefits of e.g. GLSL syntax highlighting. This is rather easy to fix: you could load the string from a file. Pretty much all tutorials do that after they've taught you how to use strings.</p>
<p>Second, the GLSL code is compiled whenever you run the program. This means that if you have an error in it, you will only know at runtime of the C++ program. It would be much more convenient to compile it ahead-of-time.</p>
<p>It turns out that complete compilation is not possible, but it can at least be compiled ahead-of-time into an intermediary format called SPIR-V. More details and examples can be found <a target="_blank" href="https://www.geeks3d.com/20200211/how-to-load-spir-v-shaders-in-opengl/">here</a> and <a target="_blank" href="https://www.khronos.org/opengl/wiki/SPIR-V#Example">here</a>.</p>
<p>So I followed those examples and used SPIR-V shaders instead of GLSL ones. I therefore have another compilation step in the CMakefile. In Make adding a new type of target would be straightfoward - Make is agnostic to what language is used and how things are compiled - but in CMake, adding a non-C++ target needs the <code>add_custom_command</code> command:</p>
<pre><code class="lang-plaintext">function(compile_spirv in_file out_file)
    add_custom_command(
        OUTPUT ${out_file}
        COMMAND glslc ${in_file} -o ${out_file}
        DEPENDS ${in_file}
        VERBATIM # enables escaping; generally a good practice
    )
endfunction()

# see https://jeremimucha.com/2021/05/cmake-managing-resources/
compile_spirv(${CMAKE_CURRENT_SOURCE_DIR}/shader.frag fragment_shader.spv)
compile_spirv(${CMAKE_CURRENT_SOURCE_DIR}/shader.vert vertex_shader.spv)

add_custom_target(resources ALL DEPENDS vertex_shader.spv fragment_shader.spv)
</code></pre>
<p>By commit <a target="_blank" href="https://www.khronos.org/opengl/wiki/SPIR-V#Example">c7c3849</a>, everything works and I have a GLFW window. So far the window displays precisely nothing, but it's already a good first step.</p>
<h2 id="heading-displaying-things-with-opengl">Displaying things with OpenGL</h2>
<p>We'll follow Anton's triangle tutorials. As the name says, this is used to display a triangle; however, at the end of tutorial, he mentions that displaying a square can be done easily by replacing 3 by 4 in some places. I tried to display a square, but it didn't work, so I decided to display 2 triangles per cell instead.</p>
<p>When displaying the grid, the following question comes up: Are we using a finite volume method (i.e. each point in the Grid is a cell) or a finite difference or element method (i.e. each point is a node?) For now I will pretend they are cells. If we end up using a finite volume method, just pretend we're using the dual grid.</p>
<p>There are one fewer row and one fewer column of grid cells than there are nodes (we have 100x100 nodes and therefore a 99x99 grid of cells delimited by these nodes). Therefore, the list of triangles to display is an array</p>
<pre><code class="lang-c++">GLfloat triangles[grid_to_render-&gt;rows()<span class="hljs-number">-1</span>][grid_to_render-&gt;cols()<span class="hljs-number">-1</span>][<span class="hljs-number">2</span>][<span class="hljs-number">3</span>][<span class="hljs-number">3</span>];
</code></pre>
<p>i.e. (rows - 1) x (columns - 1) cells, each cell having two triangles, each triangle consisting of 3 vertices, each vertex consisting of 3 coordinates. (At first I forgot the -1 and had some weird values displayed in the OpenGL window.)</p>
<p>We then pass the array to OpenGL and render them all using</p>
<pre><code class="lang-c++">glDrawArrays(GL_TRIANGLES, <span class="hljs-number">0</span>, <span class="hljs-keyword">sizeof</span>(triangles) / <span class="hljs-keyword">sizeof</span>(triangles[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>][<span class="hljs-number">0</span>][<span class="hljs-number">0</span>])); <span class="hljs-comment">//should be [0][0][0][0]</span>
</code></pre>
<p>You will notice that <code>sizeof(triangles[0][0][0][0])</code> represents one vertex (i.e. 3 coordinates), therefore the number passed to OpenGL is the number of vertices, not the number of triangles. At first I passed the number of triangles ( <code>sizeof(triangles) / sizeof(triangles[0][0][0])</code>) and was surprised why only 100x33.3 columns were rendered, instead of the 100x100 I was expecting.</p>
<p>We can add optimizations later - for example, right now each node is a vertex of 6 different triangles and is therefore passed 6 times to OpenGL. Maybe there's a way to optimize this (using an option called <code>GL_TRIANGLE_FAN</code> seems to be a possible solution), but I haven't found any easy way, so for now I stuck with the unoptimized version.</p>
<p>One improvement that is necessary, however, is perspective. Right now I don' t even see what vertices are at what height. Perspective is typically done using the vertex shader.</p>
<p>Tutorials such as <a target="_blank" href="https://learnopengl.com/Getting-Started/Coordinate-Systems">https://learnopengl.com/Getting-Started/Coordinate-Systems</a> usually do not have a fixed transformation in the shader; they pass this as a parameter to the shader. In shader language, this looks like this</p>
<pre><code class="lang-plaintext">#version 410 core
layout (location = 0) in vec3 aPos;

uniform mat4 view; // parameters
uniform mat4 projection;

void main()
{
   gl_Position = projection * view * vec4(aPos, 1.0);
}
</code></pre>
<p>It turns out that this is not possible when compiling for Vulkan - so I need to change it a bit, see <a target="_blank" href="https://stackoverflow.com/questions/73756580/why-does-vulkan-forbid-uniforms-that-are-not-uniform-blocks">here</a> for more details and rationale. I also needed to bump up the #version to 420 because otherwise "binding" wasn't supported.</p>
<pre><code class="lang-plaintext">#version 420 core
layout (location = 0) in vec3 aPos;

layout(binding = 0) uniform Projection {
    mat4 view;
    mat4 projection;
} projection;

void main()
{
   gl_Position = projection.projection * projection.view * vec4(aPos, 1.0);
}
</code></pre>
<p>We need first to create these parameters (which are all represented as 4x4 rotation matrices) on the CPU, then upload them to the GPU. To create the matrices, the easiest way is to use <code>glm</code>, a header-only math library designed for working with OpenGL. The library and instructions on how to integrate it with CMake can be found <a target="_blank" href="https://github.com/g-truc/glm">here</a>.</p>
<p>It turns out that the changes we made to the vertex shader affect also how we pass data to it. Typically, tutorials upload single parameters using a function called <code>glUniformMatrix4fv</code>; however we can't use it, because we have a parameter block, and therefore have to use another API called uniform buffer object (UBO). The UBO-related code can be found <a target="_blank" href="https://github.com/Warggr/waves-on-cuda/blob/6701233898f8a06482e9808da57fb1aa16623580/src/main.cpp#L274">here</a>.</p>
<p>Finally, adding a moveable camera (following the <a target="_blank" href="https://learnopengl.com/Getting-started/Camera">learnopengl.com tutorial</a>) was faster and worked better than just trying to add a perspective on my own. Two notable changes between my code and the tutorial:</p>
<ul>
<li><p>The tutorial is not object-oriented, while my code tries to do everything with the <code>MyGLFW</code> object. This is a problem for callbacks: GLFW callbacks must be functions, while I need them to be methods bound to the <code>MyGLFW</code> object to access (and update) its attributes. In C++, we can't just get a bound method and use it as a function pointer (contrarily to Python, where I could just use <code>setCallback(self.callback)</code> where <code>self.callback</code> is a bound method and can access all attributes of <code>self</code>). The solution was to use the function <code>glfwSetWindowUserPointer</code> (credits: <a target="_blank" href="https://stackoverflow.com/a/59633789">https://stackoverflow.com/a/59633789</a>) to associate the MyGLFW object with the window.</p>
</li>
<li><p>I found the scrolling with the mouse, as suggested in the tutorial, unnatural, so I inverted the directions:</p>
</li>
</ul>
<pre><code class="lang-c++">  yaw -= xoffset;
  pitch -= yoffset;
</code></pre>
<p>With this (commit <a target="_blank" href="https://stackoverflow.com/a/59633789">6701233</a>), I have a good enough visualization that I can recognize what's happening:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755456713031/b061073c-b8a0-4d73-9e4e-fbe300dff1ca.png" alt="Visualization of the wave with OpenGL" class="image--center mx-auto" /></p>
<p>(remember, we have a field that is 0 everywhere at first, and then a wave of height 1 enters from the right and moves leftwards for 20 timesteps - so this is exactly the visualization we were expecting). Now that the rendering is finished, I will be able to actually do some simulation / scientific computing. That will be described in a the next part.</p>
]]></content:encoded></item></channel></rss>