{"id":27057817,"url":"https://github.com/mrange/fsnicc","last_synced_at":"2025-04-05T11:33:29.024Z","repository":{"id":61609178,"uuid":"551844408","full_name":"mrange/FsNICC","owner":"mrange","description":"FsNICC","archived":false,"fork":false,"pushed_at":"2022-12-20T18:16:01.000Z","size":10456,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-02T02:09:05.050Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrange.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-10-15T08:01:29.000Z","updated_at":"2024-10-27T17:12:00.000Z","dependencies_parsed_at":"2023-01-30T01:31:08.945Z","dependency_job_id":null,"html_url":"https://github.com/mrange/FsNICC","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FFsNICC","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FFsNICC/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FFsNICC/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrange%2FFsNICC/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrange","download_url":"https://codeload.github.com/mrange/FsNICC/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247332056,"owners_count":20921849,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-04-05T11:33:24.816Z","updated_at":"2025-04-05T11:33:29.008Z","avatar_url":"https://github.com/mrange.png","language":"F#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# F# Advent 2022-12-01: Recreating STNICC 2000 1st place demo in F#\nAn [F# Advent Calendar in English 2022](https://sergeytihon.com/2022/10/28/f-advent-calendar-in-english-2022/) blog post. Big thanks to [Sergey Tihon](https://twitter.com/sergey_tihon) from his tireless running on F# weekly and F# advent event.\n\n\n## TLDR; parses a polygon file format for an Atari STE demo released in 2000\n\n![F#-NICC screenshot #1](assets/fs-nicc.jpg)\n\n### Build and run\n\n![Instagram logo](assets/small.png)[F#-NICC on instagram, turn on the sound](https://www.instagram.com/p/Cj1Fb2ZAwcK/) also available [on YouTube](https://www.youtube.com/watch?v=_KdtCkrmce4)\n\n```bash\n# In a terminal window\ncd source/FsNICC.Spectre\ndotnet run -c Release\n```\n\n![STNICC screenshot #1](assets/st-nicc-0.gif)\n![STNICC screenshot #2](assets/st-nicc-1.gif)\n\nSTNICC aka \"ST News International Christmas Coding Convention\" had a demo competition in 2000, the [winner](https://www.youtube.com/watch?v=nqVJWFNpTqA) was [Oxygene](https://demozoo.org/groups/2118/) with an amazing 3D screen for Atari STE.\n\n![STNICC screenshot #3](assets/st-nicc-2.gif)\n![STNICC screenshot #4](assets/st-nicc-3.gif)\n\nI did play a small part in providing the PRO tracker player.\n\nI much later found out that there was competition in 2019 to port the Oxygene demo on different platforms.\n\nThere are some amazing ones for:\n\n* [Atari STF](https://www.youtube.com/watch?v=8VOCbmMMteY)\n* [Atari Jaguar](https://www.youtube.com/watch?v=_arWStGbmOA)\n* [Atari Lynx](https://www.youtube.com/watch?v=PHSYXLpr3Rg)\n* [Atari 800XL](https://www.youtube.com/watch?v=rVa0ZEdwqoY)\n* [Amiga](https://www.youtube.com/watch?v=7sYhIxiizKY)\n* [386SX-DOS](https://www.youtube.com/watch?v=3LvQwSR_ekI)\n* [BBC Micro](https://www.youtube.com/watch?v=_mVI9d2Acyw)([source](https://github.com/kieranhj/stnicc-beeb/tree/master/data))\n* [PICO8](https://www.youtube.com/watch?v=dh2FuP1WYmU)\n* [SNES](https://www.youtube.com/watch?v=dOqxLBZiBRA)([No SuperFX](https://www.youtube.com/watch?v=BQE1rr1Xqf0))\n* [SEGA Mega Drive](https://www.youtube.com/watch?v=LcT1XIrklhQ)\n* [Gameboy Advanced](https://www.youtube.com/watch?v=HtuQa4g90p4)\n* [Gameboy Color](https://www.youtube.com/watch?v=3mPpFrVChFg)\n* [NES](https://www.youtube.com/watch?v=l8heNWVXfsw)\n\n## F# advent 2022\n\nI sometimes write for [F# advent](https://sergeytihon.com/2022/10/28/f-advent-calendar-in-english-2022/), usually about things that have no use to anyone but perhaps interesting to a few people out there.\n\nI was thinking, could I recreate the STNICC winner in F# for F# advent?\n\nFrom the competition invite to port the demo there was a format description as well as the original data file.\n\nSo in theory I could do it.\n\n## Parsing a 20 year old binary format\n\nBecause CPUs were a bit more limited on the old Atari STE than today [Leonard](https://demozoo.org/sceners/2527/) had precomputed all the coordinate transforms for the entire 3D scene and stored them in a compact binary format, around 600KiB. As the most common Atari STE had 1024KiB of memory that didn't leave much memory for other things like my memory hungry PRO Tracker player.\n\nSo [Leonard](https://demozoo.org/sceners/2527/) streamed the data from disk into memory on-demand while the demo was running.\n\nIn order to recreate the STNICC winner we need to parse the binary format\n\n### The format\n\nSee the [FORMAT.md](FORMAT.md) for all the details but:\n\n1. The entire scene is composed of 1,800 frames.\n2. A frame has a flag to indicate if the screen needs clearing, a palette delta and the polygons\n3. A polygon is either indexed (to save space) or non-indexed\n4. If non-indexed polygons has palette index and vertices\n5. If indexed polygons starts with all vertices then palette index and indexes to the vertices\n\n### First frame annotated\n\nIn the hope to give a better understanding I have annoted part of the first frame in the binary format.\n\n![First frame annotated](assets/format.png)\n\n1. The first byte (BLUE) is the frame flag containing 3 flags. In this case all 3 flags are true so we should clear the screen, load a palette delta and use the indexed polygon mode.\n2. Next are 2 bytes (GREEN) that is a bit mask indicating what palette entries should be updated. In this case 8 bits are set out of 16 meaning we should read the next 8*16 bits(2 bytes) as colors.\n3. Palette colors (PURPLE) 8*16 bits(2 bytes). More detail below on how to convert Atari STE colors to RGB colors.\n4. At the start of the polygon data in indexed mode is a byte (red) telling us how many vertices are following. In this case 0xD8(216) vertices\n5. Vertex data (YELLOW). Each vertex are a 2 byte value where the first byte is the coordinate and the second y coordinate.\n6. Polygon prefix (GREEN). This has 3 special values that tells us if we are done, or should go to next frame or next page and frame. Since this is not one of the special values we split 0x74 into two nibbles 7 and 4. 7 is the color palette entry to use and 4 are how many vertices are in this polygon.\n7. Vertex index (CYAN). 4 bytes that references vertex 0x23, 0, 1 and 2 in the vertex data above. The polygon should be closed.\n\n### F# file model\n\nThis can in F# be represented like this:\n\n```fsharp\n// Atari STE used RGB colors but only had 4096\n//  Only the low nibble in each byte used\ntype [\u003cStruct\u003e] RGB         =\n  {\n    Red     : byte\n    Green   : byte\n    Blue    : byte\n  }\n// The vertices were 2 bytes, meaning the 3D\n//  model didn't utilize the full 320x200 resolution of the screen\n//  but was limited to 256x200\ntype [\u003cStruct\u003e] Vertex2D    =\n  {\n    X   : byte\n    Y   : byte\n  }\n// The index of the color in the palette\ntype [\u003cStruct\u003e] ColorIndex  =\n  {\n    Index   : byte\n  }\n// The palette of Atari STE was 16 colors\n//  The first (0) being the background color\ntype [\u003cStruct\u003e] PaletteItem =\n  {\n    ColorIndex  : ColorIndex\n    Color       : RGB\n  }\n// The polygon has an index to the color to use\n//  and the vertices\n//  While the model has an indexed mode as well\n//  when parsing I converted to non-indexed as\n//  a Win10 machine don't struggle with a few extra KiBs.\ntype [\u003cStruct\u003e] Polygon     =\n  {\n    ColorIndex  : ColorIndex\n    Vertices    : Vertex2D array\n  }\n// The frame\n//  ClearScreen is used to tell if we should clear screen\n//    between frames. Clearing screen takes time so why do it if\n//    it's not needed?\n//  PaletteDelta is an array of 0..16 elements\n//    It's used to update the current palette of 16 colors\n//    Just overwrite the color at the index of the palette item\n//  Polygons is an array of polygons for the frame\ntype [\u003cStruct\u003e] Frame =\n  {\n    ClearScreen   : bool\n    PaletteDelta  : PaletteItem array\n    Polygons      : Polygon array\n  }\n// The entire scene is just all frames\ntype [\u003cStruct\u003e] Scene =\n  {\n    Frames        : Frame array\n  }\n```\n\nOnce parsed we \"just\" render frame by frame to recreate the STNICC winner.\n\n### The parser\n\nI thought a binary parser combinator makes sense, like so:\n\n```fsharp\n// A 'T BinaryReader takes a byte array and an index as input.\n//  It computes a value of 'T and returns it plus the updated index\ntype 'T BinaryReader = byte array -\u003e int -\u003e struct ('T*int)\n```\n\nWhen parsing text files it's common to return an option or something similar as it's common in text parsers to attempt different parsers to find that one that matches. Binary parsers tend to be more read a value that tells you what the following bytes represent, pick the correct parser and parse the bytes. If the input is incorrect we just throw and stop the entire parsing process.\n\nThe format consists of either byte or word (2 bytes) values so we need parsers for thus:\n\n```fsharp\n  let inline bbyte () : uint8 BinaryReader = fun bs i -\u003e\n    struct (bs.[i], i + 1)\n\n  let inline bword () : uint16 BinaryReader = fun bs i -\u003e\n    // Atari STE uses Motorola 68000 CPU which stores number in big-endian\n    let word =\n        ((uint16 bs.[i]) \u003c\u003c\u003c 8)\n      + (uint16 bs.[i +  1])\n    struct (word, i + 2)\n```\n\nTo this I added the computation expression builder:\n\n```fsharp\n  type BinaryReaderBuilder () =\n    class\n      member inline x.Bind  ( [\u003cInlineIfLambda\u003e] t  : _ BinaryReader\n                            , [\u003cInlineIfLambda\u003e] uf : _ -\u003e _ BinaryReader\n                            ) : _ BinaryReader = fun bs i -\u003e\n        let struct (tv, ti) = t bs i\n        uf tv bs ti\n\n      member inline x.Combine ( [\u003cInlineIfLambda\u003e] t : _ BinaryReader\n                              , [\u003cInlineIfLambda\u003e] u : _ BinaryReader\n                              ) : _ BinaryReader =  fun bs i -\u003e\n        let struct (_, ti) = t bs i\n        u bs ti\n\n      member inline x.MergeSources  ( [\u003cInlineIfLambda\u003e] t : _ BinaryReader\n                                    , [\u003cInlineIfLambda\u003e] u : _ BinaryReader\n                                    ) : _ BinaryReader =  fun bs i -\u003e\n        let struct (tv, ti) = t bs i\n        let struct (uv, ui) = u bs ti\n        struct (struct (tv, uv), ui)\n\n      member inline x.BindReturn    ( [\u003cInlineIfLambda\u003e] t : _ BinaryReader\n                                    , [\u003cInlineIfLambda\u003e] f\n                                    ) : _ BinaryReader =  fun bs i -\u003e\n        let struct (tv, ti) = t bs i\n        struct (f tv, ti)\n\n      member inline x.Return  ( v : 'T\n                              ) : 'T BinaryReader =\n        bvalue v\n      member inline x.ReturnFrom  (br : 'T BinaryReader\n                                  ): 'T BinaryReader =\n        br\n    end\n  let breader = BinaryReaderBuilder()\n```\n\nMy hope by making the functions inline and `[\u003cInlineIfLambda\u003e]` is to reduce overhead from the parser combinator. The problem I found was that it didn't really help for `Bind` that much but at least it improved it in those cases where applicatives could be used (`MergeSources` and `BindReturn`).\n\n### Parsing Atari STE RGB colors\n\nThe RGB color is a 16bit word that looks something like this: `0x0FA3`. The last 3 nibbles represent RGB. On Atari STE the full nibble was used given access to `16*16*16 =\u003e 4096` colors. The Atari STE was backwards compatible with Atari ST that had just 512 colors available (only 3 out of 4 bits was used in each nibble).\n\nThus `0x0777` on Atari ST was full white, on the Atari STE `0x0FFF` was full white. In order to be backwards compatible `0x0777` on Atari STE should be close to full white. Because of that bit 3 in each nibble is actually bit 0 and bit 0-2 are shifted left 1 step.\n\nThis will complicate stuff for us 20+ year later.\n\n```fsharp\n  let inline brgb () =\n    breader {\n      // Helper function to process a nibble\n      let inline cp c i =\n        let c = int c\n        // Extract nibble at bit pos i\n        let cp    = (c \u003e\u003e\u003e i) \u0026\u0026\u0026 0xF\n        // Atari ST color\n        // Left shift lowest 3 bits 1 step ie multiply by 2\n        let c     = (cp \u0026\u0026\u0026 0x7) \u003c\u003c\u003c 1\n        // Atari STE extended color\n        // Extract bit 3 and downshift it to bit0, add it to Atari ST color bits\n        let c     = c + ((cp \u003e\u003e\u003e 3) \u0026\u0026\u0026 0x1)\n        byte c\n\n      // Read the 16 bit color word\n      let! c = bword ()\n      return { Red = cp c 8; Green = cp c 4; Blue = cp c 0 }\n    }\n```\n\nThe Atari STE had a 16 color palette. Each frame has an optional palette delta. The palette delta starts with a 16 bit mask telling us what entries in the palette should be updated and following are RGB color values for each bit set to 1 in the mask.\n\n```fsharp\n  let bpaletteDelta : PaletteItem array BinaryReader =\n    breader {\n      // Read the 16 bit bitmask which palette items are to be updated.\n      let! bitmask  = bword ()\n      let bitmask   = uint32 bitmask\n      // Count the number of bits in the bitmask.\n      let bitCount  = popCount bitmask |\u003e int\n      // brepeat is a parser primitive that let's us repeat any\n      // parser n times and return the result as an array\n      let! rgbs     = brepeat bitCount (brgb ())\n      let pds       = Array.zeroCreate bitCount\n      // Zip the bit selection with the rgb values\n      let rec loop bitmask i j (rgbs : _ array) (pds : _ array)=\n        // Bitmask is 0 we are done\n        if bitmask \u003c\u003e 0u then\n          let i =\n            if (bitmask \u0026\u0026\u0026 0x8000u) \u003c\u003e 0u then\n              // Highest bit is 1 indicating we should update the current paltte index (j) with current color (i)\n              pds.[i] \u003c- { ColorIndex = { Index = j }; Color = rgbs.[i] }\n              i + 1\n            else\n              // Otherwise skip\n              i\n          // Shift mask to left to process next bit\n          loop (bitmask \u003c\u003c\u003c 1) i (j + 1uy) rgbs pds\n      loop bitmask 0 0uy rgbs pds\n      return pds\n    }\n```\n\nThis delta will then be used when updating the palette when we are rendering frame.\n\n### A sense of failure in abstraction\n\nWhile I got the parser working in the end I feel that I haven't found the right abstractions. My main problem was that while parsing the polygons they are each prefixed with a value that either says that following bytes are a polygon or the frame is complete and even if we should jump to the next 64KiB page in the file.\n\nI suppose the reason for the 64KiB pages in the file is because of the original demo streaming from disk. I think it uses two 64KiB buffers. One buffer is being rendered and the other is being populated from disk. When the demo reaches flip page command it renders the other buffer and the first buffer is populated from disk.\n\nBut that polygon parse result modifies how the frame parser should behave, which complicated it for me.\n\nThe polygon parser looks like this:\n```fsharp\n  let bpolygon =\n    // brepeatPrefix repeats a parser depending on the prefix\n    // The prefix either say read next value or exit the loop\n    brepeatPrefixed\n      (bpolygonDescriptor ())\n      (fun (ci, vc) -\u003e\n        breader {\n          let! vs = bvertices (int vc)\n          return polygon ci vs\n        }\n      )\n```\n\nAbove doesn't look too bad but I feel the signature of the `brepeatPrefixed` is too obscure:\n\n```fsharp\n  // Continue means continue to apply the parser with the seed value 'S\n  // Stop means stop parsing and transform accumulated value 'T to 'U\n  type PrefixResult\u003c'S, 'T, 'U\u003e =\n    | Continue  of  'S\n    | Stop      of  ('T -\u003e 'U)\n\n  let inline brepeatPrefixed\n    // Prefix parser\n    ([\u003cInlineIfLambda\u003e] t   : PrefixResult\u003c'S, 'T array, 'U\u003e BinaryReader )\n    // Value parser accepting a seed value of 'S producing 'T\n    ([\u003cInlineIfLambda\u003e] uf  : 'S -\u003e 'T BinaryReader                       )\n    : 'U BinaryReader = fun bs i -\u003e\n```\n\nI feel it is too clunky and not generic enough.\n\nThe first parser `t` parses the prefix and returns either a `Continue` value with a seed value given to the second parameter (for example how many vertices should be read for the following polygon). If we are done it returns a `Stop` value with a function that transforms the accumulated value (a polygon array for example) into the final parser value. This is to allow the prefix parser to send one of three variants to the frame parser, read Next frame, flip page and read Next frame or Stop.\n\n```fsharp\n  // Ready the polygon descriptor prefix\n  let bpolygonDescriptor () =\n    breader {\n      let! pd = bbyte ()\n      return\n        match pd with\n        // 3 special cases of polygon descriptors that stops\n        //  reading polygons\n        | 0xFFuy  -\u003e Stop Next\n        | 0xFEuy  -\u003e Stop NextPage\n        | 0xFDuy  -\u003e Stop Done\n        | _       -\u003e\n          // Extract polygon color index\n          let ci = (pd \u003e\u003e\u003e 4) \u0026\u0026\u0026 0xFuy\n          // Extract number of vertices in the polygon\n          let vc = pd \u0026\u0026\u0026 0xFuy\n          // Read following bytes as a polygon\n          Continue ({ Index = ci }, vc)\n    }\n```\n\nIf someone has some ideas on how to make it less clunky and/or increase the generality of the abstraction I would love to hear it.\n\n## Rendering STNICC in 2022\n\nTo give the proper retro feeling I felt it to be a great idea to render the entire thing in the terminal.\n\nPrinting to the terminal can be slow and colors are not portable. Luckily there are portable and libraries like [Terminal.GUI](https://github.com/gui-cs/Terminal.Gui) and [Spectre.Console](https://github.com/spectreconsole/spectre.console).\n\n### Spectre.Console\nI went with [Spectre.Console](https://github.com/spectreconsole/spectre.console) as I used it to render 2D particle constraints systems in the terminal before.\n\nSetting up a 2D canvas (a ridiculously entertaining idea) in the terminal is very easy with [Spectre.Console](https://github.com/spectreconsole/spectre.console).\n\n```fsharp\n  // The dimensions of the Atari STE graphics area in STNICC\n  let w = 256\n  let h = 200\n\n  let canvas = Canvas (w, h)\n  let updater (ctx : LiveDisplayContext) =\n      // Red color\n      let red = Spectre.Console.Color (255uy, 0uy, 0uy)\n      for i = 0 to 199 do\n        // Draw a line of red pixels\n        canvas.SetPixel(i, i, red) |\u003e ignore\n        // Refresh screen between each pixel drawn\n        ctx.Refresh()\n  AnsiConsole.Live(canvas).Start(updater)\n```\n\n### Drawing polygons with ImageSharp\n\nAs great as [Spectre.Console](https://github.com/spectreconsole/spectre.console) is [Spectre.Console](https://github.com/spectreconsole/spectre.console) doesn't support drawing polygons and a polygon filler function is a pain to implement. I know, I tried and failed to implement one for the old Atari STE.\n\nInstead, I lifted in [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp). As my work here is licensed under Apache License V2 I can choose that option when picking license under [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) split license.\n\nThis makes the updater function for the terminal straight forward:\n\n```fsharp\n  let updater (ctx : LiveDisplayContext) =\n    // Create an RGB image matching the Canvas width and height\n    use image = new Image\u003cRgb24\u003e (w, h)\n    // Draw each frame\n    for frame = 0 to sscene.Frames.Length - 1 do\n      // Mutator function\n      let filler (ctx : IImageProcessingContext) =\n        let f = sscene.Frames.[frame]\n        // Some frames rely on the previous frame not being cleared to look right\n        if f.ClearScreen then\n          ctx.Clear (Color ()) |\u003e ignore\n        // Draw each polygon\n        for p in f.Polygons do\n          ctx.FillPolygon (p.Fill, p.Points) |\u003e ignore\n      image.Mutate filler\n\n      for y = 0 to h - 1 do\n        for x = 0 to w - 1 do\n          // Grab an image pixel\n          let c = image[x, y]\n          // Convert to Spectre color\n          let sc = Spectre.Console.Color (c.R, c.G, c.B)\n          // Draw it in the Spectre canvas\n          canvas.SetPixel(x, y, sc) |\u003e ignore\n      ctx.Refresh()\n```\n\nThe frames however are not the same model as produced by the scene parser. Instead, the preprocessing step converts it into a specialized model to fit rendering with Spectre + ImageSharp.\n\n```fsharp\n\n// Everything precomputed so we just can render the polygon with ImageSharp\ntype [\u003cStruct\u003e] SpectrePolygon =\n  {\n    Fill        : Color\n    Points      : PointF array\n  }\n\n// Dropped the palette delta as the actual color is now part of the polygon\ntype [\u003cStruct\u003e] SpectreFrame =\n  {\n    ClearScreen : bool\n    Polygons    : SpectrePolygon array\n  }\n\ntype [\u003cStruct\u003e] SpectreScene =\n  {\n    Frames      : SpectreFrame array\n  }\n\nlet toSpectreScene (scene : Scene) : SpectreScene =\n  // Maps from parsed RGB to ImageSharp Color\n  let inline toColor (rgb : RGB) =\n    let inline convert c = (c \u003c\u003c\u003c 4) + c\n    Color.FromRgb (convert rgb.Red, convert rgb.Green, convert rgb.Blue)\n\n  // Atari STE had 16 color palette, all of them black to begin with\n  let palette = Array.zeroCreate 16\n\n  let mapPolygon (polygon : Polygon) : SpectrePolygon =\n    // Map the parsed byte*byte vertices to float32*float32 vertices\n    let points =\n      polygon.Vertices\n      |\u003e Array.map (fun v -\u003e PointF (float32 v.X, float32 v.Y))\n    {\n      // Lookup the color in the current palette\n      Fill    = palette.[int polygon.ColorIndex.Index]\n      Points  = points\n    }\n\n  let mapFrame (frame : Frame) : SpectreFrame =\n    // Updates the palette from the palette delta\n    for pi in frame.PaletteDelta do\n      palette.[int pi.ColorIndex.Index] \u003c- toColor pi.Color\n    let polygons =\n      frame.Polygons\n      |\u003e Array.map mapPolygon\n    {\n      ClearScreen = frame.ClearScreen\n      Polygons    = polygons\n    }\n\n  let frames =\n    scene.Frames\n    |\u003e Array.map mapFrame\n\n  { Frames = frames }\n```\n\nWith that we are done and can observe the silliness by running the program in a terminal\n\n```bash\n# In a terminal window\ncd source/FsNICC.Spectre\ndotnet run -c Release\n```\n\n![F#-NICC screenshot #1](assets/fs-nicc.jpg)\n\n*Merry christmas!*\n\n## Appendix 0 - Rendering STNICC in WPF\n\nFor Windows users wanting \"high fidelity\" graphics there is also provided a WPF project that uses the same input to render STNICC in WPF.\n\n```bash\n# Only works on Windows\ncd source/FsNICC.WPF\ndotnet run -c Release\n```\n\n\nWorks along similar principles as the [Spectre.Console](https://github.com/spectreconsole/spectre.console) example.\n\nHowever, F# is a bit WPF resistant so for a C# developer used to WPF it might look odd but that is because XAML isn't support for F# AFAIK.\n\nA challenge with WPF is how to trigger a redraw of the screen at 60FPS. I never really found a brilliant way to do it so I rely on WPF animations that I use to trigger an invalidate of the visual and then I render the STNICC polygons in `OnRender`.\n\n```fsharp\ntype SceneElement (scene : WPFScene) =\n  class\n    inherit UIElement ()\n\n    // Setup the dependency property descriptor for the Time property,\n    //  this dependency property we will animate and on each change the visual\n    //  will be invalidated\n    static let timeProperty =\n      let pc = PropertyChangedCallback SceneElement.TimePropertyChanged\n      let md = PropertyMetadata (0., pc)\n      DependencyProperty.Register (\"Time\", typeof\u003cfloat\u003e, typeof\u003cSceneElement\u003e, md)\n\n    // Invalidates the visual\n    static member TimePropertyChanged (d : DependencyObject) (e : DependencyPropertyChangedEventArgs) =\n      let g = d :?\u003e SceneElement\n      g.InvalidateVisual ()\n\n    static member TimeProperty = timeProperty\n\n    member x.Time = x.GetValue SceneElement.TimeProperty :?\u003e float\n\n    // Starts the animation for the the Time property\n    member x.Start () =\n      let b   = 0.\n      let e   = 1E9\n      let dur = Duration (TimeSpan.FromSeconds (e - b))\n      let ani = DoubleAnimation (b, e, dur) |\u003e freeze\n      x.BeginAnimation (SceneElement.TimeProperty, ani);\n\n    override x.OnRender dc =\n      let time  = x.Time\n      // Render Stuff!\n      ()\n```\n## Appendix 1 - Debugging and testing the parser\n\nEven if I think the binary parser combinators help writing a correct parser it is still bit-fiddling and very easy to screw up.\n\nSo early on in the process I built functions to write the parsed model into an intended text stream.\n\nThis text stream is committed to git: `assets/scene1.txt`\n\nThis helped me alot during the initial parsing so that I could have an easy text format to see if the parsed values I produced made sense.\n\nBut it still provides value for me when I do refactorings to the code, for example optimizations or extending it with functionality to split complex polygons into simple ones.\n\nAfter the refactoring I regenerate the text file and make sure that the model changes only contain the expected changes.\n\nA neat little trick I think for when one are implementing parsers where you like to check the parser model in its entirety after refactoring yet want to make easy to establish a new baseline after determining the refactoring produced the right changes to the model (commit the updated text file).\n\nI wrote a little indented streamer writer combinator library.  I found the library surprisingly slow but at least it produces the right result :).\n\nI suppose I should debug it in depth to find the bottleneck someday.\n\n## Appendix 2 - My contribution to STNICC 2000\nAs mentioned I made a small contribution to STNICC by providing the PRO tracker player.\n\nIf anyone is interested in arcane Atari topic I wrote up a blog explaining the history for the [PRO tracker player](https://github.com/mrange/pt_src3).\n\n## Appendix 3 - Portable fast bit population count.\n\nAn interesting problem is how to count the number of bits in a word effectively. For this parser performance it's not a problem but a fast bit counter is so wonderfully opaque and obscure I just had to include it.\n\nTrivially one writes a loop that checks if the lowest bit is set, increments a counter if set and then shifts the word right 1 step.\n\nTrivial but boring so instead one can do this:\n\n```fsharp\n  let popCount (i : uint32) : uint32 =\n    // From: http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel\n    let mutable v = i\n    v \u003c- v - ((v \u003e\u003e\u003e 1) \u0026\u0026\u0026 0x55555555u)\n    v \u003c- (v \u0026\u0026\u0026 0x33333333u) + ((v \u003e\u003e\u003e 2) \u0026\u0026\u0026 0x33333333u)\n    ((v + (v \u003e\u003e\u003e 4) \u0026\u0026\u0026 0xF0F0F0Fu) * 0x1010101u) \u003e\u003e\u003e 24\n```\n\nWonderful, just wonderful. I really recommend the [bithacks](http://graphics.stanford.edu/~seander/bithacks.html) site.\n\nIf we know a program always executes on x86 we could use the [x86 intrinsics](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.x86.popcnt.popcount?view=net-7.0#system-runtime-intrinsics-x86-popcnt-popcount(system-uint32)) to count bits as this is supported by all x86 CPUs.\n\n## Appendix 4 - Unfinished work\n\nTo support modern hardware we should triangulate the polygons.\n\nTriangulating simple polygons (no intersections) are reasonable easy thanks to the two ears theorem. The problem is the STNICC triangles are not all of them simple.\n\nSo I spent a lot of time identifying if a polygon is not simple (if lines are intersecting) and then split them.\n\nIn the end I ran out of energy before the deadline for the blog post and abandoned the work before I finished the triangulation algorithm.\n\nYou find traces of the effort from this in the scene parser to detect convexity, simples and splitting complex polygons into simple ones.\n\n## Appendix 5 - What is a 'nibble'?\n\nI was asked by my buddy [Thindal](https://www.twitch.tv/thindal): What is a nibble? He figured it out by himself but it's not common knowledge.\n\nA nibble is a 4-bit number that can hold values 0-15. The nibble was the word sized used in the 1971 legendary CPU [Intel 4004](https://en.wikipedia.org/wiki/Intel_4004). Later CPUs introduced byte (8-bit) as the word size and while modern CPUs uses much bigger word size we still talk about bytes while nibbles are largely forgotten.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrange%2Ffsnicc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrange%2Ffsnicc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrange%2Ffsnicc/lists"}