<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Portfolio on jairrs</title><link>https://jairsantana.com/portfolio/</link><description>Recent content in Portfolio on jairrs</description><generator>Hugo -- 0.156.0</generator><language>en-us</language><atom:link href="https://jairsantana.com/portfolio/index.xml" rel="self" type="application/rss+xml"/><item><title>Computer Graphics: Curves, Animation, and Rendering</title><link>https://jairsantana.com/portfolio/computer-graphics/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://jairsantana.com/portfolio/computer-graphics/</guid><description>Spline modeling, skeletal deformation, cloth simulation, ray tracing, and shadow mapping in C++ and OpenGL.</description><content:encoded><![CDATA[<p>For MIT&rsquo;s 6.4400 Computer Graphics course, I implemented a series of assignments covering spline curves and surfaces, skeletal animation, physically-based simulation, ray tracing, and shadow mapping. Most of the work used GLOO, the course&rsquo;s lightweight C++ OpenGL framework, with GLM for vector and matrix operations.</p>
<p>Here&rsquo;s a small overview of what I learned.</p>
<h2 id="modeling-curves-and-surfaces">Modeling Curves and Surfaces</h2>
<p>In Assignment 1, I implemented cubic Bézier and B-Spline curves, conversion between their control-point representations, and bicubic tensor product patches. This gave me a better understanding of non-polygonal representations for surfaces. Since the patches are continuous and differentiable, we can easily get the normal for lighting as well.</p>
<h2 id="attaching-a-mesh-to-a-skeleton">Attaching a Mesh to a Skeleton</h2>
<p>Assignment 2 covered hierarchical transforms and skeletal subspace deformation, also called linear blend skinning. I built a node hierarchy and attached a mesh using per-vertex joint weights. Rotating a parent joint carries its descendants with it, while the weights let a vertex respond to several joints at once.</p>
<p>The approach is to first move a vertex from the character&rsquo;s bind pose into a joint&rsquo;s bind-pose coordinate frame, then move it back using that joint&rsquo;s current transform. I also passed accumulated transforms down the scene tree when collecting objects for rendering, avoiding repeated walks up the hierarchy. I believe this is very similar (if not identical) to how videogames handle object rendering trees.</p>
<details class="article-details">
    <summary>Code: one joint&#39;s contribution to a vertex</summary>
    <div class="article-details-content">
        <div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-cpp" data-lang="cpp"><span style="display:flex;"><span>glm<span style="color:#ff7b72;font-weight:bold">::</span>mat4x4 <span style="color:#ff7b72;font-weight:bold">&amp;</span>bind_pose_joint_to_character_inverse_ <span style="color:#ff7b72;font-weight:bold">=</span> Bs_[j];
</span></span><span style="display:flex;"><span>glm<span style="color:#ff7b72;font-weight:bold">::</span>mat4x4 <span style="color:#ff7b72;font-weight:bold">&amp;</span>joint_to_character <span style="color:#ff7b72;font-weight:bold">=</span> joint_to_character_matrices[j];
</span></span><span style="display:flex;"><span>glm<span style="color:#ff7b72;font-weight:bold">::</span>vec4 position_contribution <span style="color:#ff7b72;font-weight:bold">=</span> (attachment_weights_[i][j] <span style="color:#ff7b72;font-weight:bold">*</span>
</span></span><span style="display:flex;"><span>joint_to_character <span style="color:#ff7b72;font-weight:bold">*</span> bind_pose_joint_to_character_inverse_ <span style="color:#ff7b72;font-weight:bold">*</span> bind_pose_position);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>new_positions<span style="color:#ff7b72;font-weight:bold">-&gt;</span>at(i) <span style="color:#ff7b72;font-weight:bold">+=</span> glm<span style="color:#ff7b72;font-weight:bold">::</span>vec3(position_contribution);
</span></span></code></pre></div><p>Summing these weighted contributions gives the deformed vertex position.</p>

    </div>
</details>

<h2 id="simulating-motion-with-particles-and-springs">Simulating Motion with Particles and Springs</h2>
<p>For Assignment 3, I implemented forward Euler, explicit trapezoidal, and fourth-order Runge–Kutta integration, then used them for circular motion, a spring-connected pendulum, and wireframe cloth. The particle system combines gravity, drag, and spring forces. The integrator then advances its positions and velocities over time.</p>
<p>The cloth is a grid with structural springs connecting neighbors, shear springs connecting diagonals, and flex springs spanning two grid edges. These resist stretching, diagonal collapse, and bending. This was more of a programming exercise, but it was still cool to see how to make a cloth simulation.</p>
<h2 id="rendering-with-rays">Rendering with Rays</h2>
<p>We were tasked with building a CPU ray tracer. I implemented sphere, plane, and triangle intersections, Phong lighting, shadow rays, and recursive reflections. Rays are transformed into object space for intersection, while normals return to world space using the inverse transpose of the object&rsquo;s transform.</p>
<details class="article-details">
    <summary>Code: tracing a reflection</summary>
    <div class="article-details-content">
        <div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-cpp" data-lang="cpp"><span style="display:flex;"><span><span style="color:#ff7b72">if</span>(bounces <span style="color:#ff7b72;font-weight:bold">&gt;</span> <span style="color:#a5d6ff">0</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// ray bounces
</span></span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> glm<span style="color:#ff7b72;font-weight:bold">::</span>vec3 ray_bounce_origin    <span style="color:#ff7b72;font-weight:bold">=</span> hit_position <span style="color:#ff7b72;font-weight:bold">+</span> EPSILON <span style="color:#ff7b72;font-weight:bold">*</span> eye_reflection;
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> glm<span style="color:#ff7b72;font-weight:bold">::</span>vec3 ray_bounce_direction <span style="color:#ff7b72;font-weight:bold">=</span> eye_reflection;
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> Ray       <span style="color:#d2a8ff;font-weight:bold">ray_bounce</span>(ray_bounce_origin, ray_bounce_direction);
</span></span><span style="display:flex;"><span>  HitRecord bounce_record <span style="color:#ff7b72;font-weight:bold">=</span> HitRecord();
</span></span><span style="display:flex;"><span>  color <span style="color:#ff7b72;font-weight:bold">+=</span> material_specular_color <span style="color:#ff7b72;font-weight:bold">*</span> TraceRay(ray_bounce, bounces <span style="color:#ff7b72;font-weight:bold">-</span> <span style="color:#a5d6ff">1</span>, bounce_record);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>eye_reflection</code> is the reflected viewing-ray direction. The small offset keeps the new ray from immediately intersecting the surface it just left.</p>

    </div>
</details>

<h2 id="casting-shadows-in-opengl">Casting Shadows in OpenGL</h2>
<p>Assignment 5 involved texture mapping and shadows from a directional light. I rendered the scene from the light&rsquo;s viewpoint into a depth texture, then sampled that texture while shading the camera&rsquo;s view. If a fragment is farther from the light than the stored depth, another surface is blocking it.</p>
<p>This involved framebuffer objects, texture sampling, coordinate transforms, and GLSL. A small depth bias helps prevent a surface from incorrectly shadowing itself because of numerical error.</p>
<details class="article-details">
    <summary>Code: comparing against the shadow map</summary>
    <div class="article-details-content">
        <div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-glsl" data-lang="glsl"><span style="display:flex;"><span><span style="color:#ff7b72">vec3</span> proj_coords <span style="color:#ff7b72;font-weight:bold">=</span> (world_to_light_ndc_matrix <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#ff7b72">vec4</span>(world_position, <span style="color:#a5d6ff">1.0</span>)).xyz;
</span></span><span style="display:flex;"><span>proj_coords <span style="color:#ff7b72;font-weight:bold">=</span> proj_coords <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">0.5</span> <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">0.5</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">float</span> this_depth     <span style="color:#ff7b72;font-weight:bold">=</span> proj_coords.z;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">float</span> occluder_depth <span style="color:#ff7b72;font-weight:bold">=</span> texture(shadow_texture, proj_coords.xy).r;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">float</span> bias <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0.005</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">if</span>(occluder_depth <span style="color:#ff7b72;font-weight:bold">&lt;</span> this_depth <span style="color:#ff7b72;font-weight:bold">-</span> bias) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> <span style="color:#ff7b72">vec3</span>(<span style="color:#a5d6ff">0.0</span>, <span style="color:#a5d6ff">0.0</span>, <span style="color:#a5d6ff">0.0</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div>
    </div>
</details>

<p>These assignments preceded my final project, <a href="/posts/grass/">emulating Ghost of Tsushima&rsquo;s procedural grass in Godot</a>.</p>
]]></content:encoded></item><item><title>Flock Herder: Interactive Boids on FPGAs</title><link>https://jairsantana.com/portfolio/flock-herder/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://jairsantana.com/portfolio/flock-herder/</guid><description>Stereo vision, fixed-point motion, and a live camera overlay on a pair of FPGAs.</description><content:encoded><![CDATA[<p>This project entailed the construction of an interactive visual program where the user can herd flocks of virtual birds known as &ldquo;boids&rdquo; — on-screen models which behave as birds in a flock, fish in a school, or mammals in a herd — which follow a simple behavioral model based on what each individual on-screen boid can perceive around them. The intended full model includes alignment with nearby flock mates as well as movement towards and around a target in the user&rsquo;s hand.</p>
<p>Updating a singular boid&rsquo;s position requires the normalization of several vectors, which presents the need to develop a resource-efficient and low-latency module for vector normalizations. To acquire the three-dimensional position of the target, we implemented stereo vision using a pair of FPGAs — each with their own camera — which communicate with each other through the SPI protocol. Using the target&rsquo;s positional information, we computed the positions of the boids every frame in three dimensions and overlaid them on the camera footage via a double frame buffer.</p>
<p>The double frame buffer also presented a unique challenge since, due to overlaying the boids over the camera feed, there was a limited amount of additional pixels/DRAM requests that could be made every frame, amounting to roughly 25% of the screen&rsquo;s area.</p>
<p>For this class project, we committed to having multiple boids follow the target, moving every frame and being rendered at their computed positions. To demonstrate proper depth calculation, we modified every boid&rsquo;s size based on its depth. The implemented boid update module includes only the target-following force; alignment, cohesion, and separation remain future work.</p>
<figure>
    <img loading="lazy" src="/images/flock-herder/stereo-setup.png"
         alt="Physical setup showing a main FPGA and a stereo FPGA, each with a camera pointed at a shared tracking object"/> <figcaption>
            <p>The physical setup: two cameras attached to a pair of Urbana FPGAs.</p>
        </figcaption>
</figure>

<h2 id="contributions">Contributions</h2>
<p>This was a final project for MIT&rsquo;s 6.2050 Digital Systems Laboratory, built with Sreeja Thipireddy. I was responsible for the implementation of the normalizer, boid manager, and boid update modules. Sreeja was responsible for the implementation of the inter-FPGA communication protocol, depth calculation module, double frame buffer, and modifications to the traffic generator. The renderer module was implemented in collaboration.</p>
<h2 id="vector-normalization">Vector Normalization</h2>
<p>To compute the necessary vectors in the boid computations, it is necessary to normalize vectors. To normalize a vector, it&rsquo;s sufficient to multiply the vector&rsquo;s components by the reciprocal of the vector&rsquo;s magnitude.</p>
<p>For a vector <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="bold">v</mi><mo>=</mo><mo stretchy="false">(</mo><msub><mi>v</mi><mi>x</mi></msub><mo separator="true">,</mo><msub><mi>v</mi><mi>y</mi></msub><mo separator="true">,</mo><msub><mi>v</mi><mi>z</mi></msub><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">\mathbf{v} = (v_x, v_y, v_z)</annotation></semantics></math></span>, we first compute its squared magnitude:</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>x</mi><mo>=</mo><msubsup><mi>v</mi><mi>x</mi><mn>2</mn></msubsup><mo>+</mo><msubsup><mi>v</mi><mi>y</mi><mn>2</mn></msubsup><mo>+</mo><msubsup><mi>v</mi><mi>z</mi><mn>2</mn></msubsup><mo separator="true">,</mo><mspace width="2em"/><mover accent="true"><mi mathvariant="bold">v</mi><mo>^</mo></mover><mo>=</mo><mi mathvariant="bold">v</mi><mfrac><mn>1</mn><msqrt><mi>x</mi></msqrt></mfrac><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">
x = v_x^2 + v_y^2 + v_z^2, \qquad \hat{\mathbf{v}} = \mathbf{v}\frac{1}{\sqrt{x}}.
</annotation></semantics></math></span><p>This lets us compute one reciprocal square root and reuse it for all three components.</p>
<h3 id="initial-approximation">Initial Approximation</h3>
<p>We can compute an initial approximation to the reciprocal of the square root via the position of the number&rsquo;s most significant bit.</p>
<p>Using <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi><mo>≈</mo><msup><mn>2</mn><mi>b</mi></msup></mrow><annotation encoding="application/x-tex">x \approx 2^b</annotation></semantics></math></span>, we obtain the initial approximation
</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mn>1</mn><msqrt><mi>x</mi></msqrt></mfrac><mo>≈</mo><msup><mn>2</mn><mrow><mo>−</mo><mi>b</mi><mi mathvariant="normal">/</mi><mn>2</mn></mrow></msup><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">
\frac{1}{\sqrt{x}} \approx 2^{-b/2}.
</annotation></semantics></math></span><p>For a nonzero integer <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span>, the position <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>b</mi></mrow><annotation encoding="application/x-tex">b</annotation></semantics></math></span> of its most significant bit is
</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>b</mi><mo>=</mo><mrow><mi mathvariant="normal">B</mi><mi mathvariant="normal">I</mi><mi mathvariant="normal">T</mi><mi mathvariant="normal">_</mi><mi mathvariant="normal">C</mi><mi mathvariant="normal">O</mi><mi mathvariant="normal">U</mi><mi mathvariant="normal">N</mi><mi mathvariant="normal">T</mi></mrow><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mo>−</mo><mn>1</mn><mo>−</mo><mrow><mi mathvariant="normal">C</mi><mi mathvariant="normal">L</mi><mi mathvariant="normal">Z</mi></mrow><mo stretchy="false">(</mo><mi>x</mi><mo stretchy="false">)</mo><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">
b = \mathrm{BIT\_COUNT}(x) - 1 - \mathrm{CLZ}(x).
</annotation></semantics></math></span><p>I implemented a custom combinational module with a tree-based approach which computes the count of leading zeroes. The result for any n-bit number can be derived from the result of its left and right halves. If the upper half of the number contains a 1 bit, the result comes from that half. Otherwise, the result is the width of the upper half plus the leading-zero count of the lower half. This gives us a combinational tree for finding the CLZ. The implementation starts with 32 two-bit pairs: <code>00</code> has two leading zeroes, <code>01</code> has one, and a pair starting with <code>1</code> has none. Five merge layers combine these counts into the result for the full 64-bit input. In <code>clz_merge</code>, the highest bit indicates that a whole block is zero; the remaining bits encode its leading-zero count.
<details class="article-details">
    <summary>Code: tree-structured leading-zero counter</summary>
    <div class="article-details-content">
        <p>From <code>main/hdl/vec.sv</code>. The source references this  <a href="https://electronics.stackexchange.com/questions/196914/verilog-synthesize-high-speed-leading-zero-count">leading-zero count tree discussion</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-systemverilog" data-lang="systemverilog"><span style="display:flex;"><span><span style="color:#ff7b72">module</span> clz_merge #(
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">parameter</span> WIDTH
</span></span><span style="display:flex;"><span>) (
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">input</span> <span style="color:#ff7b72">wire</span> [WIDTH<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">1</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>] left,
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">input</span> <span style="color:#ff7b72">wire</span> [WIDTH<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">1</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>] right,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">output</span> <span style="color:#ff7b72">logic</span> [<span style="color:#79c0ff;font-weight:bold">WIDTH:</span><span style="color:#a5d6ff">0</span>] out
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">logic</span> both_full, left_not_full;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">assign</span> both_full <span style="color:#ff7b72;font-weight:bold">=</span> left[WIDTH<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">1</span>] <span style="color:#ff7b72;font-weight:bold">&amp;</span> right[WIDTH<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">1</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">assign</span> left_not_full <span style="color:#ff7b72;font-weight:bold">=</span> left[WIDTH<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">1</span>] <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">1</span><span style="color:#a5d6ff">&#39;b0</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">logic</span> [WIDTH<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">2</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>] right_chopped;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">assign</span> right_chopped <span style="color:#ff7b72;font-weight:bold">=</span> right[WIDTH<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">2</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">logic</span> [<span style="color:#79c0ff;font-weight:bold">WIDTH:</span><span style="color:#a5d6ff">0</span>] result;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">always_comb</span> <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span>(both_full) <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>            result <span style="color:#ff7b72;font-weight:bold">=</span> {<span style="color:#a5d6ff">1</span><span style="color:#a5d6ff">&#39;b1</span>, {WIDTH{<span style="color:#a5d6ff">1</span><span style="color:#a5d6ff">&#39;b0</span>}}};
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">end</span> <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">if</span>(left_not_full) <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>            result <span style="color:#ff7b72;font-weight:bold">=</span> {<span style="color:#a5d6ff">1</span><span style="color:#a5d6ff">&#39;b0</span>, left};
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">end</span> <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>            result <span style="color:#ff7b72;font-weight:bold">=</span> {<span style="color:#a5d6ff">2</span><span style="color:#a5d6ff">&#39;b01</span>, right_chopped};
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">assign</span> out <span style="color:#ff7b72;font-weight:bold">=</span> result;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">endmodule</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">module</span> clz_64 (
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">input</span> <span style="color:#ff7b72">wire</span> [<span style="color:#a5d6ff">63</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>] data,
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">output</span> <span style="color:#ff7b72">logic</span> [<span style="color:#a5d6ff">6</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>] out
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">logic</span> [<span style="color:#a5d6ff">1</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>] bit_pair [<span style="color:#a5d6ff">31</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">logic</span> [<span style="color:#a5d6ff">6</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>] assembly_layer [<span style="color:#a5d6ff">5</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>] [<span style="color:#a5d6ff">31</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">genvar</span> j;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">generate</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">for</span> (j <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>; j <span style="color:#ff7b72;font-weight:bold">&lt;</span> <span style="color:#a5d6ff">32</span>; j<span style="color:#ff7b72;font-weight:bold">++</span>) <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">assign</span> bit_pair[j] <span style="color:#ff7b72;font-weight:bold">=</span> data[<span style="color:#a5d6ff">2</span><span style="color:#ff7b72;font-weight:bold">*</span>j<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#a5d6ff">1</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">2</span><span style="color:#ff7b72;font-weight:bold">*</span>j];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">assign</span> assembly_layer[<span style="color:#a5d6ff">0</span>][j] <span style="color:#ff7b72;font-weight:bold">=</span>
</span></span><span style="display:flex;"><span>                (bit_pair[j] <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">2</span><span style="color:#a5d6ff">&#39;b00</span>) <span style="color:#ff7b72;font-weight:bold">?</span> <span style="color:#a5d6ff">6</span><span style="color:#a5d6ff">&#39;b000010</span> <span style="color:#ff7b72;font-weight:bold">:</span>
</span></span><span style="display:flex;"><span>                (bit_pair[j] <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">2</span><span style="color:#a5d6ff">&#39;b01</span>) <span style="color:#ff7b72;font-weight:bold">?</span> <span style="color:#a5d6ff">6</span><span style="color:#a5d6ff">&#39;b000001</span> <span style="color:#ff7b72;font-weight:bold">:</span>
</span></span><span style="display:flex;"><span>                                         <span style="color:#a5d6ff">6</span><span style="color:#a5d6ff">&#39;b000000</span> ;
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">endgenerate</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">genvar</span> layer, i;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">generate</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">for</span> (layer <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">1</span>; layer <span style="color:#ff7b72;font-weight:bold">&lt;</span> <span style="color:#a5d6ff">6</span>; layer<span style="color:#ff7b72;font-weight:bold">++</span>) <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">for</span> (i <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>; i <span style="color:#ff7b72;font-weight:bold">&lt;</span> (<span style="color:#a5d6ff">32</span> <span style="color:#ff7b72;font-weight:bold">&gt;&gt;</span> (layer <span style="color:#ff7b72;font-weight:bold">-</span> <span style="color:#a5d6ff">1</span>)); i <span style="color:#ff7b72;font-weight:bold">+=</span> <span style="color:#a5d6ff">2</span>) <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>                clz_merge #(
</span></span><span style="display:flex;"><span>                    .WIDTH(layer <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">1</span>)
</span></span><span style="display:flex;"><span>                ) m_clz_merge (
</span></span><span style="display:flex;"><span>                    .left(assembly_layer[layer <span style="color:#ff7b72;font-weight:bold">-</span> <span style="color:#a5d6ff">1</span>][i <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">1</span>][<span style="color:#79c0ff;font-weight:bold">layer:</span><span style="color:#a5d6ff">0</span>]),
</span></span><span style="display:flex;"><span>                    .right(assembly_layer[layer <span style="color:#ff7b72;font-weight:bold">-</span> <span style="color:#a5d6ff">1</span>][i][<span style="color:#79c0ff;font-weight:bold">layer:</span><span style="color:#a5d6ff">0</span>]),
</span></span><span style="display:flex;"><span>                    .out(assembly_layer[layer][i <span style="color:#ff7b72;font-weight:bold">&gt;&gt;</span> <span style="color:#a5d6ff">1</span>][layer<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#a5d6ff">1</span><span style="color:#ff7b72;font-weight:bold">:</span><span style="color:#a5d6ff">0</span>])
</span></span><span style="display:flex;"><span>                );
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">end</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">endgenerate</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">assign</span> out <span style="color:#ff7b72;font-weight:bold">=</span> assembly_layer[<span style="color:#a5d6ff">5</span>][<span style="color:#a5d6ff">0</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">endmodule</span>
</span></span></code></pre></div>
    </div>
</details>
</p>
<p>The squared components of the vector are shifted right by <code>FP_SHIFT</code> before being added, so <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi></mrow><annotation encoding="application/x-tex">x</annotation></semantics></math></span> still has <code>FP_SHIFT</code> fractional bits. If <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>F</mi><mo>=</mo><mrow><mi mathvariant="normal">F</mi><mi mathvariant="normal">P</mi><mi mathvariant="normal">_</mi><mi mathvariant="normal">S</mi><mi mathvariant="normal">H</mi><mi mathvariant="normal">I</mi><mi mathvariant="normal">F</mi><mi mathvariant="normal">T</mi></mrow></mrow><annotation encoding="application/x-tex">F = \mathrm{FP\_SHIFT}</annotation></semantics></math></span> and <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi></mrow><annotation encoding="application/x-tex">X</annotation></semantics></math></span> is the stored squared magnitude, its real value is <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mi mathvariant="normal">/</mi><msup><mn>2</mn><mi>F</mi></msup></mrow><annotation encoding="application/x-tex">X/2^F</annotation></semantics></math></span>. The stored reciprocal-square-root estimate therefore needs the scale factor</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msup><mn>2</mn><mi>F</mi></msup><mfrac><mn>1</mn><msqrt><mrow><mi>X</mi><mi mathvariant="normal">/</mi><msup><mn>2</mn><mi>F</mi></msup></mrow></msqrt></mfrac><mo>≈</mo><msup><mn>2</mn><mrow><mo stretchy="false">(</mo><mn>3</mn><mi>F</mi><mo>−</mo><mi>b</mi><mo stretchy="false">)</mo><mi mathvariant="normal">/</mi><mn>2</mn></mrow></msup><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">
2^F\frac{1}{\sqrt{X/2^F}} \approx 2^{(3F-b)/2}.
</annotation></semantics></math></span><p>The code uses <code>e = (3*FP_SHIFT - b) &gt;&gt; 1</code> for this exponent. It zero-pads the squared magnitude to 64 bits before counting leading zeroes, then uses <code>b = 63 - len_sqr_clz</code> to find its most significant bit. The clocked assignment <code>y_pipe[2] &lt;= (1 &lt;&lt; e) &lt;&lt; FP_SHIFT</code> gives the estimate an extra <code>FP_SHIFT</code> bits, which are removed by <code>y_pipe_shifted</code> before it is used in the next multiplication.</p>
<details class="article-details">
    <summary>Code: fixed-point initial estimate</summary>
    <div class="article-details-content">
        <div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-systemverilog" data-lang="systemverilog"><span style="display:flex;"><span>clz_64 m_clz_64 (
</span></span><span style="display:flex;"><span>        .data({{(<span style="color:#a5d6ff">64</span><span style="color:#ff7b72;font-weight:bold">-</span>DOT_PRODUCT_WIDTH){<span style="color:#a5d6ff">1</span><span style="color:#a5d6ff">&#39;b0</span>}},len_sqr_pipe[<span style="color:#a5d6ff">1</span>]}),
</span></span><span style="display:flex;"><span>        .out(len_sqr_clz)
</span></span><span style="display:flex;"><span>    ); <span style="color:#8b949e;font-style:italic">// 1
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">assign</span> b <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">63</span> <span style="color:#ff7b72;font-weight:bold">-</span> len_sqr_clz; <span style="color:#8b949e;font-style:italic">// 1
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">assign</span> e <span style="color:#ff7b72;font-weight:bold">=</span> (<span style="color:#a5d6ff">3</span><span style="color:#ff7b72;font-weight:bold">*</span>FP_SHIFT <span style="color:#ff7b72;font-weight:bold">-</span> b) <span style="color:#ff7b72;font-weight:bold">&gt;&gt;</span> <span style="color:#a5d6ff">1</span>; <span style="color:#8b949e;font-style:italic">// 1
</span></span></span></code></pre></div>
    </div>
</details>

<h3 id="newton-raphson-refinement">Newton-Raphson Refinement</h3>
<p>To improve this initial guess, I computed several iterations of a Newton-Raphson approximation scheme. Through a C simulation I found that 4 iterations was enough to achieve visually satisfactory results.</p>
<p>For an estimate <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>y</mi><mi>n</mi></msub></mrow><annotation encoding="application/x-tex">y_n</annotation></semantics></math></span> of <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><mi mathvariant="normal">/</mi><msqrt><mi>x</mi></msqrt></mrow><annotation encoding="application/x-tex">1/\sqrt{x}</annotation></semantics></math></span>, the iteration is</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>y</mi><mrow><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msub><mo>=</mo><mfrac><msub><mi>y</mi><mi>n</mi></msub><mn>2</mn></mfrac><mrow><mo fence="true">(</mo><mn>3</mn><mo>−</mo><mi>x</mi><msubsup><mi>y</mi><mi>n</mi><mn>2</mn></msubsup><mo fence="true">)</mo></mrow><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">
y_{n+1} = \frac{y_n}{2}\left(3 - xy_n^2\right).
</annotation></semantics></math></span><p>At the desired value, <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi><msubsup><mi>y</mi><mi>n</mi><mn>2</mn></msubsup><mo>=</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">xy_n^2 = 1</annotation></semantics></math></span>, so the correction factor is one. Each iteration employs only multiplication and substraction operations. In fixed-point arithmetic, the products also need to be shifted to account for the fractional bits. I split each iteration across three clock cycles: square the current estimate, multiply by the squared magnitude, and apply the correction. <code>THREE_HALFS</code> stores 1.5 in fixed-point format. The expression <code>half_ay2_pipe[i] &gt;&gt; (2 * FP_SHIFT + 1)</code> accounts for the product&rsquo;s extra fractional bits and divides it by two. The generated stages implement the equivalent form <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>y</mi><mrow><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msub><mo>=</mo><msub><mi>y</mi><mi>n</mi></msub><mo stretchy="false">(</mo><mn>1.5</mn><mo>−</mo><mi>x</mi><msubsup><mi>y</mi><mi>n</mi><mn>2</mn></msubsup><mi mathvariant="normal">/</mi><mn>2</mn><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">y_{n+1} = y_n(1.5 - xy_n^2/2)</annotation></semantics></math></span>.
<details class="article-details">
    <summary>Code: three-cycle Newton-Raphson iteration</summary>
    <div class="article-details-content">
        <div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-systemverilog" data-lang="systemverilog"><span style="display:flex;"><span><span style="color:#ff7b72">genvar</span> pipe_idx;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">generate</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">for</span>(pipe_idx <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">3</span>; pipe_idx <span style="color:#ff7b72;font-weight:bold">&lt;</span> <span style="color:#a5d6ff">3</span> <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">3</span><span style="color:#ff7b72;font-weight:bold">*</span>ITERATIONS; pipe_idx <span style="color:#ff7b72;font-weight:bold">+=</span> <span style="color:#a5d6ff">3</span>) <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">always_ff</span> @( <span style="color:#ff7b72">posedge</span> clk ) <span style="color:#ff7b72">begin</span>
</span></span><span style="display:flex;"><span>                y2_pipe[pipe_idx] <span style="color:#ff7b72;font-weight:bold">&lt;=</span> y_pipe_shifted[pipe_idx<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">1</span>] <span style="color:#ff7b72;font-weight:bold">*</span> y_pipe_shifted[pipe_idx<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">1</span>]; <span style="color:#8b949e;font-style:italic">// 3*i
</span></span></span><span style="display:flex;"><span>                <span style="color:#8b949e;font-style:italic">// y2_pipe[pipe_idx+1] &lt;= y2_pipe[pipe_idx];
</span></span></span><span style="display:flex;"><span>                <span style="color:#8b949e;font-style:italic">// y2_pipe[pipe_idx+2] &lt;= y2_pipe[pipe_idx+1];
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#8b949e;font-style:italic">// half_ay2_pipe[pipe_idx] &lt;= half_ay2_pipe[pipe_idx-1];
</span></span></span><span style="display:flex;"><span>                half_ay2_pipe[pipe_idx<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#a5d6ff">1</span>] <span style="color:#ff7b72;font-weight:bold">&lt;=</span> (len_sqr_pipe[pipe_idx] <span style="color:#ff7b72;font-weight:bold">*</span> y2_pipe[pipe_idx]); <span style="color:#8b949e;font-style:italic">// 3*i + 1
</span></span></span><span style="display:flex;"><span>                <span style="color:#8b949e;font-style:italic">// half_ay2_pipe[pipe_idx+2] &lt;= half_ay2_pipe[pipe_idx+1];
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                y_pipe[pipe_idx] <span style="color:#ff7b72;font-weight:bold">&lt;=</span> y_pipe[pipe_idx<span style="color:#ff7b72;font-weight:bold">-</span><span style="color:#a5d6ff">1</span>];
</span></span><span style="display:flex;"><span>                y_pipe[pipe_idx<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#a5d6ff">1</span>] <span style="color:#ff7b72;font-weight:bold">&lt;=</span> y_pipe[pipe_idx];
</span></span><span style="display:flex;"><span>                y_pipe[pipe_idx<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#a5d6ff">2</span>] <span style="color:#ff7b72;font-weight:bold">&lt;=</span> ($signed(y_pipe_shifted[pipe_idx<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#a5d6ff">1</span>]) <span style="color:#ff7b72;font-weight:bold">*</span> ($signed({<span style="color:#a5d6ff">1</span><span style="color:#a5d6ff">&#39;b0</span>, THREE_HALFS}) <span style="color:#ff7b72;font-weight:bold">-</span> $signed({<span style="color:#a5d6ff">1</span><span style="color:#a5d6ff">&#39;b0</span>, half_ay2_pipe_shifted[pipe_idx<span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#a5d6ff">1</span>]}))); <span style="color:#8b949e;font-style:italic">// 3*i + 2
</span></span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">end</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">end</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">endgenerate</span>
</span></span></code></pre></div>
    </div>
</details>
</p>
<h3 id="precision-and-pipeline-cost">Precision and Pipeline Cost</h3>
<p>In the evaluation described in our final report, using an 11-bit integer range and an <code>FP_SHIFT</code> of 12, the computed vectors were within 10% of the desired vectors, which was enough accuracy for our application. This is an observed result for the evaluated inputs rather than a guarantee for every fixed-point input; sufficiently small vectors can have their squared components truncated to zero.</p>
<p>When synthesized for an 11-bit integer range and 12 bits of precision, the normalizer consumes 48 DSP slices. The latency of the normalizer is <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>3</mn><mo>+</mo><mn>3</mn><mi>I</mi></mrow><annotation encoding="application/x-tex">3 + 3I</annotation></semantics></math></span>, where <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>I</mi></mrow><annotation encoding="application/x-tex">I</annotation></semantics></math></span> is the number of iterations used for the inverse square root solver. While it is fully pipelined, all multiplications are assumed to take one cycle, which limits the range and precision of the input vectors due to the volume of operations required between frames. Too high a latency and we would not be able to finish computing updates for all boids.</p>
<h2 id="boid-computations">Boid Computations</h2>
<p>Every boid is associated with a position and velocity vector. Every frame, we add the velocity vector multiplied by a small <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">Δ</mi><mi>t</mi></mrow><annotation encoding="application/x-tex">\Delta t</annotation></semantics></math></span> to its position. The intended full behavioral model calculates the acceleration of the boid based on:</p>
<ol>
<li>Normalized offset to the target&rsquo;s position. Used in computing the target force.</li>
<li>Average direction of other boids in the perception radius. Also known as the average flock heading. Used in computing the alignment force.</li>
<li>Normalized offset to the average position of other boids in the perception radius. Also known as the flock center. Used in computing the cohesion force.</li>
<li>Average of the negative offset to the position of other boids. Also known as the separation heading. In essence, this force makes sure that boids don&rsquo;t fly over each other. Used in computing the separation force.</li>
</ol>
<p>In the full model, each of the above results is computed into a steering force by subtracting the boid&rsquo;s velocity from the influencing vector. These results are then clamped to have a maximum magnitude equal to a constant maximum steering force. These contributions to the acceleration are then summed and added to the boid&rsquo;s velocity.</p>
<p>The velocity&rsquo;s magnitude is then clamped between a constant minimum and maximum speed. This new velocity is then used next frame to update the boid&rsquo;s position.</p>
<h3 id="boid-update">Boid Update</h3>
<p>The <code>boid_update</code> module computes the boid&rsquo;s new velocity, which is passed back to the <code>boid_manager</code> module. In the submitted implementation, it only implemented the first of the four steering forces, which is responsible for leading the boid towards the target. To do this, it implements an FSM comprised of the following states:</p>
<ol>
<li><code>IDLE</code></li>
<li><code>NORMALIZE_OFFSET</code></li>
<li><code>GET_STEER</code></li>
<li><code>NORMALIZE_STEER_TARGET</code></li>
<li><code>CLAMP_STEER</code></li>
<li><code>GET_ACCELERATION</code></li>
<li><code>GET_VELOCITY</code></li>
<li><code>NORMALIZE_VELOCITY</code></li>
<li><code>CLAMP_VELOCITY</code></li>
</ol>
<p>Of relevance here is that, to clamp a vector between given minimum and maximum magnitudes, it is necessary to normalize it beforehand. Due to resource limitations, it was impossible to instantiate a separate normalizer module for each of these operations, so instead we opted for a blocking implementation during which the <code>boid_update</code> module changes the inputs to its normalizer submodule and waits for results.</p>
<p>To implement the rest of the steering forces, it would then be necessary to add stages for the computation of the necessary data as well as the necessary normalization stages before each steering force computation and subsequent clamping of the output value.</p>
<p>Our final report recorded a 51-cycle latency for <code>boid_update</code> and estimated at least 19,000 boid updates per frame on the pixel clock, including <code>boid_manager</code> overhead. In the supplied source version, the FSM takes 56 cycles after accepting an update request, before manager overhead: three 16-cycle normalization states, three two-cycle steering or clamping states, and two single-cycle states. The report&rsquo;s throughput estimate therefore needs to be recalculated for this version.</p>
<h3 id="boid-manager">Boid Manager</h3>
<p>The <code>boid_manager</code> module responsible for passing the necessary parameters to <code>boid_update</code> for every boid implements an FSM comprised of the following states:</p>
<ol>
<li><code>IDLE</code></li>
<li><code>POS_UPDATE</code></li>
<li><code>POS_UPDATE2</code></li>
<li><code>UPDATE_START</code></li>
<li><code>UPDATE_WAIT</code></li>
</ol>
<p>During the <code>IDLE</code> state, the module awaits the <code>new_frame</code> signal before saving the currently inputted target coordinates and transitioning to the <code>POS_UPDATE</code> state. In this state, the module updates all boid positions by adding their respective velocities scaled by a small positive number. Then, during <code>POS_UPDATE2</code>, all positions are boundary-checked and wrapped around the screen if necessary.</p>
<p>During <code>UPDATE_START</code>, the manager asserts the <code>data_in_valid</code> signal to the <code>boid_update</code> module and then transitions to the waiting state, where the current boid index is incremented after the <code>boid_update</code> module&rsquo;s <code>busy</code> signal goes low. This continues until all boids&rsquo; velocities have been updated, at which point the module returns to the <code>IDLE</code> state.</p>
<h2 id="design-evaluation">Design Evaluation</h2>
<h3 id="double-frame-buffer-and-rendering-bottleneck">Double Frame Buffer and Rendering Bottleneck</h3>
<p>The objective of the submitted implementation was to be able to render as many boids as possible and have them change sizes appropriately. Writing camera pixels into the frame buffer consumes most of the frame time, and the system cannot write an amount of boid pixels that surpasses the amount of cycles in the remaining frame time. This amount of pixels is roughly equal to the amount of cycles in the v-sync period.</p>
<p>This upper bounds the size and quantity of the boids by the amount of pixels for each boid when they&rsquo;re closest to the screen. In the configuration evaluated in our final report, we found that we could comfortably render 16 boids as 96 × 96 squares — or 147,456 pixels — whilst remaining under the frame time constraint. This measurement differs from the supplied renderer&rsquo;s current settings, which produce nominal square sizes of 4–49 pixels before eight-pixel width packing. This was the design&rsquo;s bottleneck and would be the first area to be improved upon in the next iteration as both the depth calculation and boid updates can handle increased throughput.</p>
<p>Additionally, we could cut off the additional pixels sent via the eight-pixel packing by modifying the strobe signal to the DDR3 controller to only write pixels included in the square region. This would remove the current rendering artifact where box widths change only in increments of eight.</p>
<h3 id="communication-protocol-and-depth-calculation">Communication Protocol and Depth Calculation</h3>
<p>Since the communication protocol only needs to communicate 22 bits for the center of mass information every frame regardless of any changes to the rest of the design, it would not need any changes or improvements. Similarly, the depth calculation module deals with known constants and is mostly a wrapper for a fixed-point divide. Even if a different camera were to be used, it would only require constant values and/or bit widths to be changed in this module.</p>
<h3 id="vector-normalization-and-boid-updates">Vector Normalization and Boid Updates</h3>
<p>The biggest source of resource allocation in the design comes from the normalizer instanced by the <code>boid_update</code> module. Forty-eight of the sixty DSP slices in the module come from the normalizer instance. Most of these come from the pipelined Newton-Raphson iterations, which instance multiplications between numbers with result bit widths of up to <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>4</mn><mtext> </mtext><mrow><mi mathvariant="normal">F</mi><mi mathvariant="normal">P</mi><mi mathvariant="normal">_</mi><mi mathvariant="normal">R</mi><mi mathvariant="normal">A</mi><mi mathvariant="normal">N</mi><mi mathvariant="normal">G</mi><mi mathvariant="normal">E</mi></mrow><mo>+</mo><mn>3</mn><mtext> </mtext><mrow><mi mathvariant="normal">F</mi><mi mathvariant="normal">P</mi><mi mathvariant="normal">_</mi><mi mathvariant="normal">S</mi><mi mathvariant="normal">H</mi><mi mathvariant="normal">I</mi><mi mathvariant="normal">F</mi><mi mathvariant="normal">T</mi></mrow><mo>+</mo><mn>2</mn></mrow><annotation encoding="application/x-tex">4\,\mathrm{FP\_RANGE} + 3\,\mathrm{FP\_SHIFT} + 2</annotation></semantics></math></span>. This results in timing and resource utilization issues for higher integer ranges and precision widths.</p>
<p>This problem could be solved by splitting each multiplication into different steps — each of them fitting into a single DSP slice. This would drastically reduce resource utilization and improve throughput at the cost of increased latency. Increased latency, however, directly influences the amount of boids that can be updated every frame due to the boid manager&rsquo;s blocking implementation of updates and the choice to instance only one normalizer for <code>boid_update</code>.</p>
<p><a href="https://github.mit.edu/6205F25/fa25-6205-team28">Project source (MIT GitHub)</a></p>
]]></content:encoded></item><item><title>Operating Systems: Memory, Networking, and Files</title><link>https://jairsantana.com/portfolio/operating-systems/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://jairsantana.com/portfolio/operating-systems/</guid><description>Copy-on-write memory, an Ethernet driver, concurrent allocation, and file-backed mappings in xv6.</description><content:encoded><![CDATA[<p>For MIT&rsquo;s 6.1810 Operating System Engineering course, I implemented a series of assignments in xv6, a small Unix-like operating system for RISC-V. The work covered virtual memory, networking, synchronization, and file systems. Working inside a kernel gave me a better understanding of what happens behind operations I normally just call from a program.</p>
<p>Here&rsquo;s a small overview of my favorite parts of OS.</p>
<h2 id="copy-on-write">Copy-On-Write</h2>
<p>I implemented copy-on-write for <code>fork</code>. Instead of immediately copying a process&rsquo;s memory, the parent and child initially share physical pages. Pages that were writable become read-only, and a write triggers a page fault that creates a private copy.</p>
<p>I added a page-table flag to distinguish these pages, reference counts to track their lifetime, and handling for writes made through the kernel&rsquo;s <code>copyout</code> function.</p>
<h2 id="nic-driver">NIC Driver</h2>
<p>I implemented transmit and receive handling for the emulated E1000 Ethernet device, along with UDP port binding and packet queues. A process waiting for a packet sleeps until the receive path wakes it up.</p>
<p>The driver uses rings of descriptors shared with the OS via MMIO. I replace received buffers before handing packets to the network stack, and reclaim transmitted buffers once the device is done with them.</p>
<details class="article-details">
    <summary>Code: waiting for a packet</summary>
    <div class="article-details-content">
        <div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#ff7b72">char</span> <span style="color:#ff7b72;font-weight:bold">*</span>packet;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">while</span>((packet <span style="color:#ff7b72;font-weight:bold">=</span> (<span style="color:#ff7b72">char</span> <span style="color:#ff7b72;font-weight:bold">*</span>) udp_port<span style="color:#ff7b72;font-weight:bold">-&gt;</span>packet_ring[udp_port<span style="color:#ff7b72;font-weight:bold">-&gt;</span>head]) <span style="color:#ff7b72;font-weight:bold">==</span> <span style="color:#a5d6ff">0</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#d2a8ff;font-weight:bold">sleep</span>(udp_port, <span style="color:#ff7b72;font-weight:bold">&amp;</span>netlock);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>sleep</code> releases the lock while the process waits, then reacquires it before returning. The loop checks the queue again after waking up.</p>

    </div>
</details>

<h2 id="letting-cpus-work-in-parallel">Letting CPUs Work in Parallel</h2>
<p>I split the physical memory allocator into per-CPU free lists, each with its own lock. When one list is empty, it can take a page from another CPU&rsquo;s list. I also implemented a reader-writer spinlock that allows multiple readers, with pending writers preventing new readers from entering.</p>
<h2 id="extending-the-file-system">Extending the File System</h2>
<p>I added doubly indirect blocks so files could grow beyond xv6&rsquo;s original limit, including the cleanup needed when truncating them. I also implemented symbolic links, following chains when opening a file and supporting <code>O_NOFOLLOW</code>.</p>
<h2 id="mapping-files-into-memory">Mapping Files into Memory</h2>
<p>I added file-backed <code>mmap</code> and <code>munmap</code>. Each process records its mapped regions, and page faults load file contents as those pages are accessed. Shared mappings write data back during unmapping, while private mappings keep changes out of the file. I also added handling for these regions across <code>fork</code>, exit, and <code>exec</code>.</p>
<p>Accessing a file through a pointer still involves allocating pages, checking permissions, and managing the underlying file&rsquo;s lifetime.</p>
]]></content:encoded></item><item><title>YGo: Peer-to-Peer Collaborative Editing</title><link>https://jairsantana.com/portfolio/ygo/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://jairsantana.com/portfolio/ygo/</guid><description>A YATA text CRDT, WebRTC editing, and a Raft-backed Go signaling service.</description><content:encoded><![CDATA[<p>YGo is a collaborative editing app that uses near real-time peer-to-peer shared editing via CRDTs and a Go backend. We built it as a final project for MIT&rsquo;s 6.5840 Distributed Systems.</p>
<p>The primary goal of YGo was to achieve availability: it is disruptive for a user to be unable to create and edit documents because all resources are currently exhausted. We designed the signaling layer around near-stateless servers, allowing for quick horizontal scaling in case of need. By “near-stateless” we mean that some state is stored in each signaling server&rsquo;s memory, but losing the peer registry does not itself erase the clients&rsquo; document state. Established peer connections can continue without signaling; connecting or reconnecting peers depends on recovering the room assignment.</p>
<p>A second goal was to achieve fault tolerance: if one of the signaling servers dies, clients should still be able to create documents and establish P2P connections, provided the backend retains a Raft quorum. Since the servers relay signaling traffic between clients, we designed the system so that every client within the same document connects to the same signaling server. Without carefully addressing the failure of a hosting server, there could be split brain in the client network, causing two or more groups of clients to be unable to communicate with each other.</p>
<h2 id="contributions">Contributions</h2>
<p>I built YGo with Carolina Moura, with whom I coauthored the final report. I worked on the client described in Section III: the YATA list and text CRDT, its interface, and the peer-to-peer synchronization protocol. The Raft and key-value server code also came from my 6.5840 labs. For this project, we adapted the storage system to use gRPC instead of the simulated RPC system provided for testing.</p>
<h2 id="system-overview">System Overview</h2>
<p>A client can query any YGo node on its assigned HTTP port to create a document, referred to as a “room,” or look up its hosting signaling server. The client then opens a WebSocket to that host and receives the list of current collaborators. Via the WebSocket, clients exchange the information needed to establish WebRTC connections within the same document. Document edits travel over WebRTC data channels rather than through the Go server.</p>
<p>On startup, each backend node starts an HTTP server, a gRPC server, a key-value instance, and a Raft instance. Upon client requests, the HTTP server calls functions provided by the Clerk, which calls the KV service over the network. The replicated state machine under the KV server uses Raft as its consensus algorithm. Raft and KV share the gRPC listener and the client connections between nodes.</p>
<figure>
    <img loading="lazy" src="/images/ygo/backend.png"
         alt="Report diagram of one YGo node: HTTP on port 8080 calls the Clerk; gRPC on port 7001 exposes KV and Raft, with the KV state machine submitting operations through the RSM"/> <figcaption>
            <p>Figure 1 from our final report: one backend node. HTTP and WebSocket traffic use port 8080; KV and Raft RPCs share port 7001. The browser-side CRDT is outside this backend diagram.</p>
        </figcaption>
</figure>

<p>The distinction between document state and room metadata is central to the design. The replicated backend stores room assignments and signaling-server heartbeats. The document itself lives in the peers&rsquo; CRDT instances. Replicating metadata therefore does not make document content durable.</p>
<h2 id="the-yata-list-and-text-crdt">The YATA List and Text CRDT</h2>
<p>We employed the YATA approach to a Conflict-free Replicated Data Type, whose basic primitive is the YATA list. Peers can operate concurrently on different versions of the list and share updates without requiring consensus amongst themselves. Convergence depends on integrating the same updates using deterministic ordering rules.</p>
<p>A list item stores its identifier, creation-time origins, mutable linked-list neighbors, content, and a deletion flag:</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>i</mi><mo>=</mo><mo stretchy="false">⟨</mo><mi>I</mi><mi>D</mi><mo separator="true">,</mo><mrow><mi mathvariant="normal">o</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">g</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">n</mi><mi mathvariant="normal">L</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">f</mi><mi mathvariant="normal">t</mi></mrow><mo separator="true">,</mo><mrow><mi mathvariant="normal">o</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">g</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">n</mi><mi mathvariant="normal">R</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">g</mi><mi mathvariant="normal">h</mi><mi mathvariant="normal">t</mi></mrow><mo separator="true">,</mo><mrow><mi mathvariant="normal">l</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">f</mi><mi mathvariant="normal">t</mi></mrow><mo separator="true">,</mo><mrow><mi mathvariant="normal">r</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">g</mi><mi mathvariant="normal">h</mi><mi mathvariant="normal">t</mi></mrow><mo separator="true">,</mo><mrow><mi mathvariant="normal">c</mi><mi mathvariant="normal">o</mi><mi mathvariant="normal">n</mi><mi mathvariant="normal">t</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">n</mi><mi mathvariant="normal">t</mi></mrow><mo separator="true">,</mo><mrow><mi mathvariant="normal">d</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">l</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">t</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">d</mi></mrow><mo stretchy="false">⟩</mo><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">
i = \langle ID,\mathrm{originLeft},\mathrm{originRight},\mathrm{left},\mathrm{right},\mathrm{content},\mathrm{deleted}\rangle.
</annotation></semantics></math></span><p>The origins refer to the IDs of the items immediately to the left and right of <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>i</mi></mrow><annotation encoding="application/x-tex">i</annotation></semantics></math></span> when it was first created. The <code>left</code> and <code>right</code> fields are mutable pointers used to maintain the local view of the linked list. A boolean flag, or tombstone, is used for deletions to preserve the structure: an item is marked deleted rather than removed, so concurrent insertions referencing its ID as an origin can still be resolved.</p>
<p>Each item is assigned an ID consisting of a pair:</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>I</mi><mi>D</mi><mo>=</mo><mo stretchy="false">(</mo><mi>f</mi><mo separator="true">,</mo><mi>c</mi><mo stretchy="false">)</mo><mo separator="true">,</mo></mrow><annotation encoding="application/x-tex">
ID = (f,c),
</annotation></semantics></math></span><p>where <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span> identifies the client and <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>c</mi></mrow><annotation encoding="application/x-tex">c</annotation></semantics></math></span> is a monotonically increasing insertion counter maintained by that client. The report described <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi></mrow><annotation encoding="application/x-tex">f</annotation></semantics></math></span> as a UUID. In the supplied implementation, the browser hashes the signaling UUID with SHA-256 and uses the first signed 32-bit word as its numeric CRDT identifier. This is an identifier scheme rather than a guarantee of uniqueness; collisions remain possible.</p>
<h3 id="deterministic-insertion">Deterministic Insertion</h3>
<p>When integrating item <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>i</mi></mrow><annotation encoding="application/x-tex">i</annotation></semantics></math></span>, the local list may already contain other insertions between <code>originLeft</code> and <code>originRight</code>, made after <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>i</mi></mrow><annotation encoding="application/x-tex">i</annotation></semantics></math></span> was created. To find its position deterministically, the algorithm traverses the local list from the left origin&rsquo;s right neighbor toward the right origin.</p>
<p>For items sharing the same left origin, it breaks ties by client identifier. An item with a smaller client identifier is resolved to the left of the incoming item. If it has the same right origin and does not sort to the left, the scan stops. The algorithm also tracks items encountered during the scan and unresolved conflicting items, so insertions whose origins lie within that region retain their relationship to preceding items. An item with a left origin outside the scanned region ends the scan. The new item is then linked immediately after the rightmost item resolved as being to its left.</p>
<p>The report defined a counter-first total order over IDs, but the insertion code uses client identifiers for the same-origin tie-break. The excerpt below shows that implemented rule and the dependency tracking directly.</p>
<details class="article-details">
    <summary>Code: deterministic insertion scan</summary>
    <div class="article-details-content">
        <p>From <code>crdt/list.ts</code>; excerpt from the supplied source.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>    <span style="color:#ff7b72">while</span> (<span style="color:#ff7b72;font-weight:bold">!</span>ID.equals(scan.id, right.id)) {
</span></span><span style="display:flex;"><span>      itemsBeforeOrigin.add(ID.key(scan.id));
</span></span><span style="display:flex;"><span>      conflictingItems.add(ID.key(scan.id));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">if</span> (ID.equals(scan.originLeft, item.originLeft)) {
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// tie-break by UUID
</span></span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (scan.id.userID <span style="color:#ff7b72;font-weight:bold">&lt;</span> item.id.userID) {
</span></span><span style="display:flex;"><span>          left <span style="color:#ff7b72;font-weight:bold">=</span> scan;
</span></span><span style="display:flex;"><span>          conflictingItems.clear();
</span></span><span style="display:flex;"><span>        } <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">if</span> (ID.equals(scan.originRight, item.originRight)) {
</span></span><span style="display:flex;"><span>          <span style="color:#ff7b72">break</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      } <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">if</span> (itemsBeforeOrigin.has(ID.key(scan.originLeft))) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (<span style="color:#ff7b72;font-weight:bold">!</span>conflictingItems.has(ID.key(scan.originLeft))) {
</span></span><span style="display:flex;"><span>          left <span style="color:#ff7b72;font-weight:bold">=</span> scan;
</span></span><span style="display:flex;"><span>          conflictingItems.clear();
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">break</span>;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      scan <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">this</span>.getItem(scan.right);
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></div>
    </div>
</details>

<p>When inserting multiple items at the same time, they may not be in dependency order. <code>applyUpdate</code> repeatedly integrates items whose origins are already known. If it drains the pending set, integration is complete; if a pass makes no progress and pending items remain, it throws an unresolved-origins error. For an item already present, it merges deletion state monotonically: an incoming tombstone can mark the existing item deleted, but cannot restore it.</p>
<p>We implemented a simple Text CRDT using the YATA list by making each insertion an individual character. Allowing arbitrary-length strings to be inserted as a single item, and splitting them only when required by later operations, remains a potential optimization.</p>
<h3 id="state-vectors-and-deletions">State Vectors and Deletions</h3>
<p>A state vector records how many insertions from each client the peer knows. In the source, the stored value is the next counter after the greatest integrated counter:</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>S</mi><mi>V</mi><mo stretchy="false">[</mo><mi>f</mi><mo stretchy="false">]</mo><mo>=</mo><munder><mrow><mi>max</mi><mo>⁡</mo></mrow><mrow><mtext>integrated items from </mtext><mi>f</mi></mrow></munder><mo stretchy="false">(</mo><mi>c</mi><mo>+</mo><mn>1</mn><mo stretchy="false">)</mo><mo separator="true">,</mo></mrow><annotation encoding="application/x-tex">
SV[f] = \max_{\text{integrated items from }f}(c+1),
</annotation></semantics></math></span><p>with zero used when none are known. <code>makeUpdate</code> sends items for which <span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>c</mi><mo>≥</mo><mi>S</mi><msub><mi>V</mi><mrow><mi mathvariant="normal">r</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">c</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">p</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">n</mi><mi mathvariant="normal">t</mi></mrow></msub><mo stretchy="false">[</mo><mi>f</mi><mo stretchy="false">]</mo></mrow><annotation encoding="application/x-tex">c \geq SV_{\mathrm{recipient}}[f]</annotation></semantics></math></span>. This representation assumes that the counter summarizes a complete prefix; it does not encode arbitrary gaps in a client&rsquo;s insertion history.</p>
<details class="article-details">
    <summary>Code: selecting items missing from a state vector</summary>
    <div class="article-details-content">
        <p>From <code>crdt/list.ts</code>; excerpt from the supplied source.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> items: <span style="color:#ff7b72">YataItem</span>[] <span style="color:#ff7b72;font-weight:bold">=</span> [];
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">const</span> item <span style="color:#ff7b72">of</span> <span style="color:#ff7b72">this</span>.itemById.values()) {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">if</span> (<span style="color:#ff7b72">this</span>.isSentinel(item)) <span style="color:#ff7b72">continue</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">const</span> known <span style="color:#ff7b72;font-weight:bold">=</span> target.<span style="color:#ff7b72">get</span>(item.id.userID) <span style="color:#ff7b72;font-weight:bold">??</span> <span style="color:#a5d6ff">0</span>;
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">if</span> (item.id.opCounter <span style="color:#ff7b72;font-weight:bold">&gt;=</span> known) {
</span></span><span style="display:flex;"><span>        items.push(item);
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></div>
    </div>
</details>

<p>Deletions do not allocate new insertion IDs. The protocol therefore sends tombstone keys separately from the insertion diff. <code>getItems</code> returns a left-to-right ordered list of items that have not been deleted, while the underlying tombstoned items remain available for resolving origins.</p>
<h2 id="peer-to-peer-synchronization">Peer-to-Peer Synchronization</h2>
<p>A group of peers sharing the same text instance communicates edits through <code>TextPeer</code>, a protocol layer that wraps the CRDT. The wire format has two message kinds: <code>stateVector</code>, carrying the sender&rsquo;s full state vector, and <code>update</code>, carrying CRDT items and tombstone keys.</p>
<h3 id="initial-sync">Initial Sync</h3>
<p>When a peer&rsquo;s data channel to another peer first opens, it calls <code>syncWithPeer</code>, which sends a state-vector message. The receiver replies with an update containing every item the sender is missing, computed with <code>makeUpdate</code>, plus every tombstoned key. Because the data channel opens on both sides, the exchange runs symmetrically so each peer can acquire the other&rsquo;s state at the time of synchronization.</p>
<h3 id="periodic-broadcast">Periodic Broadcast</h3>
<p>After initial sync, a timer in the application layer calls <code>broadcastChanges</code>. Each call builds an item diff against <code>lastBroadcastSV</code> and a deletion list from tombstones appended since the previous broadcast. If either list is nonempty, it sends the update to every connected peer and advances both broadcast cursors.</p>
<details class="article-details">
    <summary>Code: batched update broadcasts</summary>
    <div class="article-details-content">
        <p>From <code>web/src/lib/textPeer.ts</code>; excerpt from the supplied source.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>  broadcastChanges(rtc: <span style="color:#ff7b72">RTC</span>)<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">void</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> items <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">this</span>.text.makeUpdate(<span style="color:#ff7b72">this</span>.lastBroadcastSV);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> deletes <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">this</span>.tombstoneOrder.slice(
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">this</span>.lastBroadcastTombstoneCount,
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (items.length <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">0</span> <span style="color:#ff7b72;font-weight:bold">&amp;&amp;</span> deletes.length <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">0</span>) <span style="color:#ff7b72">return</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> msg: <span style="color:#ff7b72">WireMessage</span> <span style="color:#ff7b72;font-weight:bold">=</span> { kind<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;update&#34;</span>, items, deletes };
</span></span><span style="display:flex;"><span>    rtc.broadcast(JSON.stringify(msg));
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">this</span>.lastBroadcastSV <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">this</span>.text.getStateVector();
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">this</span>.lastBroadcastTombstoneCount <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">this</span>.tombstoneOrder.length;
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#34;broadcastChanges&#34;</span>, JSON.stringify(msg));
</span></span><span style="display:flex;"><span>  }
</span></span></code></pre></div>
    </div>
</details>

<p><code>handleMessage</code> applies incoming items and tombstones. In the supplied source, it leaves the broadcast cursors unchanged, allowing items and tombstones learned from one neighbor to ride out on the next broadcast to other neighbors. The cursor-advancing optimization in that method is commented out because it would suppress forwarding in a partially connected graph.</p>
<p>A useful property of this design is that the graph of peers need not be fully connected for updates to propagate. The design uses forwarding to propagate updates along paths in a connected component, provided dependencies can be integrated and messages continue to be delivered. Disconnected components cannot exchange edits until a path is restored. The browser&rsquo;s connection setup nevertheless attempts a mesh by connecting each new peer to the peers listed in its welcome message.</p>
<p>The supplied UI defaults to a two-second broadcast interval, adjustable from 200 ms to 10 seconds in 200 ms steps. These are batching settings, not measured end-to-end editing latencies. The local text changes immediately; remote visibility depends on the next broadcast, transport, and integration.</p>
<h3 id="why-broadcasting">Why broadcasting?</h3>
<p>It&rsquo;s possible to implement the peer-to-peer protocol as a pull-based protocol instead of a push-based protocol. This would entail deciding when to request updates instead of when to send them to peers. I decided to stick with a push-based protocol for several reasons. First, it is much simpler to think about and implement. Broadcasting peers need not wait for a reply and receiving peers don&rsquo;t have to send one. Second, it removes needless probing that would occur with a pull-based approach. Peers can be optimized to only broadcast when they have something to share, reducing traffic. A notification-push and pull-based approach could circumvent these kinds of issues, but there is very little gain in complicating the approach in this way.</p>
<h2 id="signaling-and-room-assignment">Signaling and Room Assignment</h2>
<p>The signaling server fulfills three purposes: it manages the creation of rooms, answers requests about which server currently hosts a room, and acts as a middleman in establishing P2P connections if it is the hosting server.</p>
<p>The storage interface provides versioned reads and conditional writes:</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>GET(k) → (value, version)
</span></span><span style="display:flex;"><span>PUT(k, v, version) → ok | ErrVersion | ErrMaybe
</span></span></code></pre></div><p>A successful write increments the version. <code>ErrVersion</code> indicates a conflicting version; <code>ErrMaybe</code> represents an uncertain outcome after a retry. This distinction matters because a failed response does not necessarily mean the original write was not applied.</p>
<p>The report described separate <code>server/&lt;addr&gt;</code> lease keys. The supplied source instead stores a serialized address-to-timestamp map under one <code>heartbeats</code> key. Each server updates its entry with a versioned read-modify-write. Room ownership is stored separately as <code>room/&lt;id&gt; → advertised address</code>.</p>
<h3 id="heartbeats-and-room-creation">Heartbeats and Room Creation</h3>
<p>Each server registers its advertised address and periodically renews its timestamp. With the supplied startup configuration, renewals are scheduled every 10 seconds and <code>Signalings</code> uses a 30-second freshness window. A server belongs to the live set when</p>
<span class="katex"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><msub><mi>t</mi><mrow><mi mathvariant="normal">l</mi><mi mathvariant="normal">o</mi><mi mathvariant="normal">c</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">l</mi></mrow></msub><mo>−</mo><msub><mi>t</mi><mrow><mi mathvariant="normal">h</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">r</mi><mi mathvariant="normal">t</mi><mi mathvariant="normal">b</mi><mi mathvariant="normal">e</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">t</mi></mrow></msub><mo>≤</mo><mn>30,000</mn><mtext> </mtext><mrow><mi mathvariant="normal">m</mi><mi mathvariant="normal">s</mi></mrow><mi mathvariant="normal">.</mi></mrow><annotation encoding="application/x-tex">
t_{\mathrm{local}} - t_{\mathrm{heartbeat}} \leq 30{,}000\ \mathrm{ms}.
</annotation></semantics></math></span><p>These values describe the implementation&rsquo;s timers; they are not a bound on failover time. Divergent wall clocks, failed renewals, retries, and delayed requests all affect detection.</p>
<p>On <code>POST /api/rooms</code>, the server lists the live signaling set, picks a host uniformly at random, generates a fresh six-character room ID, and attempts a version-zero write. The report called these IDs alphanumeric; the current generator uses uppercase letters only. The receiving server need not be the chosen host: any signaling server can create a room on behalf of another. The current creation loop generates a new ID after any non-OK result, rather than distinguishing only version conflicts.</p>
<h3 id="lookup-and-reassignment">Lookup and Reassignment</h3>
<p>On <code>GET /api/rooms/:room</code>, the server reads the room assignment and checks whether the recorded host is still in the live set. If it is, its address is returned. If its heartbeat has expired, the server picks a new host and writes it with the version observed during lookup. If another lookup raced this update, <code>ErrVersion</code> causes the check to repeat.</p>
<details class="article-details">
    <summary>Code: conditional reassignment after a host expires</summary>
    <div class="article-details-content">
        <p>From <code>server.go</code>; excerpt from the supplied source.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#6e7681">		</span><span style="color:#8b949e;font-style:italic">// The server is dead, pick a new random server</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span>newServer<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>live[rand.<span style="color:#d2a8ff;font-weight:bold">Intn</span>(len(live))]<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span>putErr<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>s.ck.<span style="color:#d2a8ff;font-weight:bold">Put</span>(<span style="color:#a5d6ff">&#34;room/&#34;</span><span style="color:#ff7b72;font-weight:bold">+</span>room,<span style="color:#6e7681"> </span>newServer,<span style="color:#6e7681"> </span>version)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>putErr<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">==</span><span style="color:#6e7681"> </span>rpc.OK<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">			</span>fmt.<span style="color:#d2a8ff;font-weight:bold">Printf</span>(<span style="color:#a5d6ff">&#34;Room %q reassigned: %q (dead) -&gt; %q\n&#34;</span>,<span style="color:#6e7681"> </span>room,<span style="color:#6e7681"> </span>server,<span style="color:#6e7681"> </span>newServer)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">			</span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span>newServer,<span style="color:#6e7681"> </span>rpc.OK<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>putErr<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">==</span><span style="color:#6e7681"> </span>rpc.ErrVersion<span style="color:#6e7681"> </span>{<span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic">// someone raced us; prob will get out in the next iteration</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">			</span><span style="color:#ff7b72">continue</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;&#34;</span>,<span style="color:#6e7681"> </span>putErr<span style="color:#6e7681">
</span></span></span></code></pre></div>
    </div>
</details>

<p>The host can die between the lookup and the client&rsquo;s connection attempt. The client discovers this when connecting fails and repeats the lookup. In the supplied frontend, reconnection starts with a 500 ms backoff, doubles on repeated failures, and caps at 10 seconds. Lookup tries the page&rsquo;s origin and a hard-coded list of three localhost servers, which would need to be replaced for deployment.</p>
<h3 id="establishing-peer-connections">Establishing Peer Connections</h3>
<p>Before upgrading a WebSocket, the server validates the room and checks that it is still the assigned host. On a successful upgrade, it assigns a fresh peer UUID, inserts the connection into its in-memory room registry, and sends a welcome message carrying that ID and the IDs of current peers.</p>
<p>Peers use signaling to exchange SDP offers, answers, and ICE candidates. The report described all peer JSON as broadcast; the source stamps the sender&rsquo;s ID and unicasts messages with a <code>to</code> field to that peer. Messages without a target are broadcast to the other peers in the room. Once WebRTC data channels are established, edits use those channels.</p>
<h3 id="recovering-from-signaling-failure">Recovering from Signaling Failure</h3>
<p>We addressed two kinds of failure. When the hosting signaling server dies, its heartbeat eventually expires. A subsequent lookup through a surviving node reassigns the room. Clients notice the WebSocket close and repeat the lookup, reconnecting to the new host. The frontend keeps its RTC instance and CRDT across the signaling outage, so the outage alone does not tear down established data channels or discard document state. Recovery behavior also depends on subsequent peer-membership messages and reconnection.</p>
<p>A server can also be reassigned away from a room while still alive if it misses enough heartbeats. It may still hold open WebSockets for rooms it no longer owns. To address this stale registry, each server runs an eviction check every 10 seconds. If it discovers it is no longer the assigned host, it closes the room&rsquo;s WebSockets, triggering client lookup and reconnection. This is a periodic recovery mechanism, not an instantaneous fencing guarantee.</p>
<h2 id="replicated-key-value-storage">Replicated Key-Value Storage</h2>
<p>For the storage system, we used the code from Lab 4 and adapted it to gRPC. We also used the lab&rsquo;s Raft code beneath the replicated state machine that sustains the KV server. Both <code>Get</code> and <code>Put</code> handlers submit operations through the RSM; the Clerk retries against other nodes when it receives <code>ErrWrongLeader</code> or cannot reach a server.</p>
<p>The KV state machine accepts a write only when its expected version matches the current value. For a new key, the expected version must be zero.</p>
<details class="article-details">
    <summary>Code: version checks in the KV state machine</summary>
    <div class="article-details-content">
        <p>From <code>kv/kv.go</code>; excerpt from the supplied source.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#6e7681">	</span>vv,<span style="color:#6e7681"> </span>ok<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">:=</span><span style="color:#6e7681"> </span>kv.store[args.Key]<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">	</span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>!ok<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">&amp;&amp;</span><span style="color:#6e7681"> </span>args.Version<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">!=</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">0</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span>reply.Err<span style="color:#6e7681"> </span>=<span style="color:#6e7681"> </span>rpc.ErrNoKey<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span><span style="color:#ff7b72">return</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">	</span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">	</span><span style="color:#ff7b72">if</span><span style="color:#6e7681"> </span>ok<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">&amp;&amp;</span><span style="color:#6e7681"> </span>vv.Version<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">!=</span><span style="color:#6e7681"> </span>args.Version<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span>reply.Err<span style="color:#6e7681"> </span>=<span style="color:#6e7681"> </span>rpc.ErrVersion<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">		</span><span style="color:#ff7b72">return</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">	</span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">	</span>kv.store[args.Key]<span style="color:#6e7681"> </span>=<span style="color:#6e7681"> </span>VersionedValue{Value:<span style="color:#6e7681"> </span>args.Value,<span style="color:#6e7681"> </span>Version:<span style="color:#6e7681"> </span>args.Version<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">+</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">1</span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">	</span>reply.Err<span style="color:#6e7681"> </span>=<span style="color:#6e7681"> </span>rpc.OK<span style="color:#6e7681">
</span></span></span></code></pre></div>
    </div>
</details>

<p>The source uses a bbolt-backed persister for Raft state and snapshot bytes. This storage supports the replicated metadata layer; it does not store the browser&rsquo;s text CRDT. KV snapshot and restore methods exist, while <code>bootRaft</code> passes <code>-1</code> as the RSM&rsquo;s snapshot threshold, disabling automatic size-triggered snapshots in this configuration.</p>
<p>The report assumed an always-available, strongly consistent storage service when explaining signaling. In practice, committing these operations requires a Raft majority. The supplied Compose configuration contains three nodes, so two are required for a quorum. Adding signaling capacity does not itself provide automatic Raft membership changes. Existing peer editing can continue independently of the backend, but room creation, lookup, and reassignment depend on the metadata service making progress.</p>
<h2 id="results-and-limitations">Results and Limitations</h2>
<p>We implemented the browser-side YATA list and character-based text wrapper, initial state exchange, periodic edit broadcasts, WebRTC signaling, versioned room assignment, heartbeat-based discovery, and periodic eviction of stale room registries. The final report described the architecture and recovery paths but did not include quantitative latency, throughput, scalability, or failover measurements. The timer values above are source configuration, and the recovery paths describe mechanisms rather than experimentally established guarantees.</p>
<p>The project has several limitations:</p>
<ul>
<li><strong>Document durability.</strong> Content is not saved to persistent storage. Once the last peer leaves a room, the document content is lost even if its room assignment remains in the database.</li>
<li><strong>Resource exhaustion.</strong> There is no authentication or rate limiting to prevent a user from creating large numbers of rooms. Room records accumulate in the metadata store; retained items and tombstones also grow with edit history.</li>
<li><strong>Wall-clock leases.</strong> Heartbeat freshness relies on each machine&rsquo;s clock. Clock differences can cause a node to be declared dead prematurely, and storing all heartbeats in one map introduces contention on that key.</li>
<li><strong>Editor scope.</strong> The supplied UI handles single-character insertion, line breaks, and single-character deletion. It rejects range edits, paste, IME composition, and other multi-character inputs.</li>
<li><strong>Update handling.</strong> Unknown tombstone keys are ignored, and unresolved insertion origins cause an exception rather than entering a persistent retry queue. These paths limit the implementation&rsquo;s tolerance of missing dependencies; connected topology alone is not an unconditional delivery or convergence guarantee.</li>
</ul>
<p>Planned work included a <code>Sync</code> operation to store CRDT state in the database, client snapshots for document persistence, authentication, rate limiting, a load balancer, and a more reliable lease implementation. Arbitrary-length text items remain another potential optimization. These features are not implemented in the supplied source.</p>
<h2 id="source-code-and-references">Source Code and References</h2>
<p><a href="https://github.com/CarolinaMoura/6.5840-final-project">Project source</a>.</p>
<p>The underlying Raft and KV code originated in my course labs. The report asks readers to obtain permission before accessing that lab repository.</p>
<p>The CRDT design followed P. Nicolaescu, K. Jahns, M. Derntl, and R. Klamma, <em>Near real-time peer-to-peer shared editing on extensible data types</em>, GROUP &lsquo;16, pp. 39–49 (<a href="https://doi.org/10.1145/2957276.2957310">DOI</a>).</p>
]]></content:encoded></item></channel></rss>