anthonypjshaw commited on
Commit
9a4efe1
Β·
verified Β·
1 Parent(s): 783fc1f

Replace mermaid diagrams with PNG renders

Browse files
Files changed (5) hide show
  1. .gitattributes +2 -0
  2. README.md +227 -276
  3. diagram_body.png +3 -0
  4. diagram_structure.png +0 -0
  5. diagram_wrapper.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ diagram_body.png filter=lfs diff=lfs merge=lfs -text
37
+ diagram_wrapper.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,276 +1,227 @@
1
- ---
2
- license: mit
3
- tags:
4
- - onnx
5
- - emulator
6
- - chip-8
7
- - retro-computing
8
- - computation
9
- - not-machine-learning
10
- pipeline_tag: other
11
- library_name: onnxruntime
12
- ---
13
-
14
- # CHIP-8 in ONNX
15
-
16
- A complete CHIP-8 emulator implemented as a pure ONNX computation graph.
17
- No custom operators, no execution-provider extensions, no Python in the
18
- hot loop β€” the entire CPU lives inside the model. Standard
19
- [ONNX Runtime](https://onnxruntime.ai/) 1.26 CPU EP runs it unmodified.
20
-
21
- This is not a machine-learning model. There are no weights, no training,
22
- no inference in the statistical sense. It is a CPU expressed as a
23
- computation graph, because it turns out ONNX has all the primitives a
24
- CPU needs: bitwise ops, indexed memory access, conditional dispatch, and
25
- a `Loop` operator that's Turing-complete with the rest of the op set.
26
-
27
- ![Snake title screen rendered by the model](example_output.gif)
28
-
29
- The image above is **the literal output of `Run()`** on `chip8_snake_demo.onnx`
30
- β€” a `uint8[90, 32, 64]` tensor returned in one call, with no inputs.
31
-
32
- ## Two models, one CPU
33
-
34
- | File | Inputs | Outputs | Notes |
35
- |---|---|---|---|
36
- | `chip8_cpu.onnx` | RAM + register state + key state + trip count | Updated RAM + register state | Load any CHIP-8 ROM into RAM, call once per game tick |
37
- | `chip8_snake_demo.onnx` | *(none β€” fully baked)* | `uint8[90, 32, 64]` frame stack | Single `Run()` returns a 90-frame movie of the Snake title screen |
38
-
39
- The two models share the same inner CPU. The demo wraps that CPU in an
40
- outer `Loop` whose body executes 30 instructions per frame and whose
41
- **scan output** is the framebuffer β€” that's how ONNX naturally accumulates
42
- "one frame per outer iteration" into a single tensor.
43
-
44
- ## How it works
45
-
46
- ### State
47
-
48
- CHIP-8 has 4 KB of RAM, sixteen 8-bit registers, a 12-bit program counter,
49
- a 12-bit index register, a tiny stack, two 8-bit timers, and a 64Γ—32
50
- monochrome display. All of it lives in three tensors that flow through the
51
- `Loop` as carried dependencies:
52
-
53
- | Tensor | Shape | Dtype | Holds |
54
- |---|---|---|---|
55
- | `regs` | `[40]` | `int32` | V0..VF, I, PC, SP, DT, ST, RNG seed, tick counter, stack[16] |
56
- | `ram` | `[4096]` | `uint8` | Program code, font, sprite data, working memory |
57
- | `display` | `[2048]` | `uint8` | 64Γ—32 framebuffer, one byte per pixel |
58
-
59
- ### The Loop body β€” one CHIP-8 instruction per iteration
60
-
61
- Each iteration of the inner `Loop` fetches, decodes, and executes one
62
- CHIP-8 instruction.
63
-
64
- ```mermaid
65
- flowchart TD
66
- A[Start iteration] --> B[Gather 2 bytes at PC from ram]
67
- B --> C[Assemble 16-bit opcode<br/>BitShift + BitwiseOr]
68
- C --> D[Extract X / Y / N / NN / NNN<br/>BitwiseAnd + BitShift]
69
- D --> E[Read Vx, Vy, I from regs<br/>Gather]
70
- E --> F["Compute *every* candidate next-state in parallel"]
71
-
72
- subgraph candidates [35 opcode candidates, all run unconditionally]
73
- direction LR
74
- F1[00E0 CLS]
75
- F2[1NNN JP]
76
- F3[6XNN LD]
77
- F4[8XY4 ADD+carry]
78
- F5[DXYN sprite XOR]
79
- F6[FX33 BCD]
80
- F7[...30 more...]
81
- end
82
-
83
- F --> candidates
84
- candidates --> G[Where-cascade:<br/>pick the candidate whose opcode<br/>pattern matches the real op]
85
- G --> H[Timer tick:<br/>decrement DT/ST every 8 iters]
86
- H --> I[Emit new regs, ram, display]
87
- I --> J[Next iteration]
88
- ```
89
-
90
- The dispatch is **branchless**: every opcode subgraph runs every iteration,
91
- and a chain of `Where` ops at the end picks the one whose pattern matches.
92
- This trades wasted work for a flat, regular graph that's much easier to
93
- read than a 35-deep nested `If` ladder β€” and it doesn't actually cost more
94
- in practice, because the per-node overhead of ONNX Runtime's `Loop` is the
95
- dominant cost anyway.
96
-
97
- ### The outer structure β€” wrapping the CPU into a movie
98
-
99
- ```mermaid
100
- flowchart TD
101
- Z[Run with no inputs] --> A0[Outer Loop: 90 iterations<br/>one per frame]
102
- subgraph outer [outer frame loop body]
103
- direction TB
104
- O0[Inner Loop: 30 iterations<br/>one CHIP-8 instruction each]
105
- O0 --> O1[Emit current display<br/>as scan output]
106
- end
107
- A0 --> outer
108
- outer --> S[Stack scan outputs:<br/>uint8 frame x 90]
109
- S --> R[Reshape to<br/>uint8 90 x 32 x 64]
110
- R --> OUT[Return frames]
111
- ```
112
-
113
- `Loop` in ONNX has two output kinds:
114
-
115
- * **Carried outputs** β€” values threaded between iterations (here: `regs`,
116
- `ram`, `display`).
117
- * **Scan outputs** β€” values *emitted per iteration* and concatenated along
118
- a new leading axis (here: the framebuffer).
119
-
120
- The movie model exploits scan outputs: one outer iteration = one frame
121
- emitted = one row of the final `frames` tensor. There is no Python loop
122
- anywhere in this pipeline; the entire 90-frame animation is produced
123
- inside a single `sess.run()` call.
124
-
125
- ### What's in the model file
126
-
127
- A `Loop` operator wrapping a single `GraphProto` body. The body has
128
- ~600 nodes β€” mostly `Gather`, `ScatterND`, `BitShift`, `BitwiseAnd`,
129
- `Equal`, and `Where`. No node is a custom op. The whole `chip8_cpu.onnx`
130
- file is ~40 KB.
131
-
132
- ```mermaid
133
- graph LR
134
- M[chip8_snake_demo.onnx] --> OL[Outer Loop<br/>trip=90]
135
- OL --> OB[Frame body]
136
- OB --> IL[Inner Loop<br/>trip=30]
137
- IL --> IB[Instruction body<br/>~600 nodes]
138
- IB --> DEC[Decode]
139
- IB --> OPS[35 opcode candidates]
140
- IB --> SEL[Where cascade]
141
- M -. baked .-> INI1[regs initializer<br/>int32 40]
142
- M -. baked .-> INI2[ram initializer<br/>uint8 4096<br/>contains snake.ch8]
143
- M -. baked .-> INI3[display initializer<br/>uint8 2048 zeros]
144
- M -. baked .-> INI4[keys initializer<br/>uint8 16 zeros]
145
- ```
146
-
147
- ## Usage
148
-
149
- ### Run the bundled demo
150
-
151
- ```python
152
- import onnxruntime as ort
153
- import numpy as np
154
- from PIL import Image
155
-
156
- sess = ort.InferenceSession("chip8_snake_demo.onnx",
157
- providers=["CPUExecutionProvider"])
158
- frames, = sess.run(None, {}) # no inputs!
159
-
160
- print(frames.shape, frames.dtype)
161
- # (90, 32, 64) uint8
162
-
163
- # Save the final frame
164
- final = (frames[-1] > 0).astype(np.uint8) * 255
165
- Image.fromarray(final, mode="L").resize((512, 256)).save("snake_frame.png")
166
- ```
167
-
168
- That's the entire usage. No tokenizer, no preprocessing, no postprocessing
169
- β€” `Run()` returns pixels.
170
-
171
- ### Load any CHIP-8 ROM into the generic CPU
172
-
173
- ```python
174
- import onnxruntime as ort
175
- import numpy as np
176
-
177
- sess = ort.InferenceSession("chip8_cpu.onnx",
178
- providers=["CPUExecutionProvider"])
179
-
180
- # Initial state
181
- def initial_ram(rom: bytes) -> np.ndarray:
182
- FONT = bytes.fromhex("F0909090F02060202070F010F080F0F010F010F0"
183
- "9090F01010F080F010F0F080F090F0F010204040"
184
- "F090F090F0F090F010F0F090F09090E090E090E0"
185
- "F0808080F0E0909090E0F080F080F0F080F08080")
186
- ram = np.zeros(4096, dtype=np.uint8)
187
- ram[0x50:0x50+80] = np.frombuffer(FONT, dtype=np.uint8)
188
- ram[0x200:0x200+len(rom)] = np.frombuffer(rom, dtype=np.uint8)
189
- return ram
190
-
191
- regs = np.zeros(40, dtype=np.int32)
192
- regs[17] = 0x200 # PC
193
- regs[21] = 0xAB # RNG seed
194
- ram = initial_ram(open("snake.ch8", "rb").read())
195
- display = np.zeros(2048, dtype=np.uint8)
196
- keys = np.zeros(16, dtype=np.uint8)
197
-
198
- # Run 30 CHIP-8 instructions per tick
199
- for tick in range(60):
200
- regs, ram, display = sess.run(None, {
201
- "regs_in": regs,
202
- "ram_in": ram,
203
- "display_in": display,
204
- "keys": keys,
205
- "trip_count": np.array(30, dtype=np.int64),
206
- })
207
-
208
- # `display` is now a uint8[2048] framebuffer β€” reshape to (32, 64) to view.
209
- ```
210
-
211
- A bundled ROM (`snake.ch8`, public domain) is included so you can try this
212
- straight away.
213
-
214
- ## Why this exists
215
-
216
- It's a question about what ONNX *is*. The ONNX operator set, once it grew
217
- `Loop`, `If`, the `Bitwise*` family (opset 18) and `ScatterND` with
218
- reduction modes, became Turing-complete in any reasonable sense of the
219
- phrase. This model demonstrates the consequence: ONNX Runtime, designed
220
- for evaluating neural networks, can also evaluate **arbitrary
221
- computations** β€” including a working game console β€” without modification.
222
-
223
- Concretely the project exists to:
224
-
225
- * Probe how far the standard ONNX op set actually goes as a general
226
- computation target.
227
- * Demonstrate that `Loop` + `Scan output` give you a clean way to express
228
- *"run a program for N steps, return one tensor per step"* in a single
229
- `Run()` call.
230
- * Provide a tiny, complete, self-contained reference for anyone who wants
231
- to do non-ML things with ONNX.
232
-
233
- If you want to play CHIP-8 games, there are a hundred better emulators.
234
- If you want to see what happens when you treat ONNX as a programming
235
- language, you're in the right place.
236
-
237
- ## Performance
238
-
239
- Measured on a Windows ARM64 laptop with ONNX Runtime 1.26 CPU EP, opset 21:
240
-
241
- | Workload | Throughput |
242
- |---|---|
243
- | CHIP-8 instructions per second | ~2,500 |
244
- | Full snake-title demo (90 frames Γ— 30 ipf = 2,700 instructions) | ~1.1 s |
245
- | Inner Loop body | ~600 ONNX nodes |
246
- | Generic CPU model file size | ~40 KB |
247
- | Snake demo model file size | ~48 KB (includes ROM) |
248
-
249
- This is plenty fast for CHIP-8 β€” most CHIP-8 games target 500–1000 Hz CPU
250
- and the model handily exceeds that. ONNX-as-a-CPU is not, however, going
251
- to be competitive with anything that wants to run a real-time emulator
252
- properly; per-node overhead in `Loop` bodies dominates everything.
253
-
254
- ## What's inside the box
255
-
256
- ```
257
- .
258
- β”œβ”€β”€ chip8_cpu.onnx # Generic CHIP-8 CPU (40 KB)
259
- β”œβ”€β”€ chip8_snake_demo.onnx # Self-contained Snake-title movie (48 KB)
260
- β”œβ”€β”€ snake.ch8 # Public-domain Snake ROM (1.4 KB)
261
- β”œβ”€β”€ example_output.gif # What you get when you Run() the demo
262
- └── README.md # This file
263
- ```
264
-
265
- ## License
266
-
267
- * Code & model files: **MIT**.
268
- * Bundled `snake.ch8` ROM: **CC0** (from
269
- [JohnEarnest/chip8Archive](https://github.com/JohnEarnest/chip8Archive)).
270
-
271
- ## Credits
272
-
273
- * CHIP-8 was created by Joseph Weisbecker in 1977 for the COSMAC VIP.
274
- * `snake.ch8` is by John Earnest, CC0.
275
- * Built with the standard ONNX op set (opset 21) and tested with
276
- ONNX Runtime 1.26.
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - onnx
5
+ - emulator
6
+ - chip-8
7
+ - retro-computing
8
+ - computation
9
+ - not-machine-learning
10
+ pipeline_tag: other
11
+ library_name: onnxruntime
12
+ ---
13
+
14
+ # CHIP-8 in ONNX
15
+
16
+ A complete CHIP-8 emulator implemented as a pure ONNX computation graph.
17
+ No custom operators, no execution-provider extensions, no Python in the
18
+ hot loop β€” the entire CPU lives inside the model. Standard
19
+ [ONNX Runtime](https://onnxruntime.ai/) 1.26 CPU EP runs it unmodified.
20
+
21
+ This is not a machine-learning model. There are no weights, no training,
22
+ no inference in the statistical sense. It is a CPU expressed as a
23
+ computation graph, because it turns out ONNX has all the primitives a
24
+ CPU needs: bitwise ops, indexed memory access, conditional dispatch, and
25
+ a `Loop` operator that's Turing-complete with the rest of the op set.
26
+
27
+ ![Snake title screen rendered by the model](example_output.gif)
28
+
29
+ The image above is **the literal output of `Run()`** on `chip8_snake_demo.onnx`
30
+ β€” a `uint8[90, 32, 64]` tensor returned in one call, with no inputs.
31
+
32
+ ## Two models, one CPU
33
+
34
+ | File | Inputs | Outputs | Notes |
35
+ |---|---|---|---|
36
+ | `chip8_cpu.onnx` | RAM + register state + key state + trip count | Updated RAM + register state | Load any CHIP-8 ROM into RAM, call once per game tick |
37
+ | `chip8_snake_demo.onnx` | *(none β€” fully baked)* | `uint8[90, 32, 64]` frame stack | Single `Run()` returns a 90-frame movie of the Snake title screen |
38
+
39
+ The two models share the same inner CPU. The demo wraps that CPU in an
40
+ outer `Loop` whose body executes 30 instructions per frame and whose
41
+ **scan output** is the framebuffer β€” that's how ONNX naturally accumulates
42
+ "one frame per outer iteration" into a single tensor.
43
+
44
+ ## How it works
45
+
46
+ ### State
47
+
48
+ CHIP-8 has 4 KB of RAM, sixteen 8-bit registers, a 12-bit program counter,
49
+ a 12-bit index register, a tiny stack, two 8-bit timers, and a 64Γ—32
50
+ monochrome display. All of it lives in three tensors that flow through the
51
+ `Loop` as carried dependencies:
52
+
53
+ | Tensor | Shape | Dtype | Holds |
54
+ |---|---|---|---|
55
+ | `regs` | `[40]` | `int32` | V0..VF, I, PC, SP, DT, ST, RNG seed, tick counter, stack[16] |
56
+ | `ram` | `[4096]` | `uint8` | Program code, font, sprite data, working memory |
57
+ | `display` | `[2048]` | `uint8` | 64Γ—32 framebuffer, one byte per pixel |
58
+
59
+ ### The Loop body β€” one CHIP-8 instruction per iteration
60
+
61
+ Each iteration of the inner `Loop` fetches, decodes, and executes one
62
+ CHIP-8 instruction.
63
+
64
+ ![Per-instruction body flowchart](diagram_body.png)
65
+
66
+ The dispatch is **branchless**: every opcode subgraph runs every iteration,
67
+ and a chain of `Where` ops at the end picks the one whose pattern matches.
68
+ This trades wasted work for a flat, regular graph that's much easier to
69
+ read than a 35-deep nested `If` ladder β€” and it doesn't actually cost more
70
+ in practice, because the per-node overhead of ONNX Runtime's `Loop` is the
71
+ dominant cost anyway.
72
+
73
+ ### The outer structure β€” wrapping the CPU into a movie
74
+
75
+ ![Outer-loop wrapper flowchart](diagram_wrapper.png)
76
+
77
+ `Loop` in ONNX has two output kinds:
78
+
79
+ * **Carried outputs** β€” values threaded between iterations (here: `regs`,
80
+ `ram`, `display`).
81
+ * **Scan outputs** β€” values *emitted per iteration* and concatenated along
82
+ a new leading axis (here: the framebuffer).
83
+
84
+ The movie model exploits scan outputs: one outer iteration = one frame
85
+ emitted = one row of the final `frames` tensor. There is no Python loop
86
+ anywhere in this pipeline; the entire 90-frame animation is produced
87
+ inside a single `sess.run()` call.
88
+
89
+ ### What's in the model file
90
+
91
+ A `Loop` operator wrapping a single `GraphProto` body. The body has
92
+ ~600 nodes β€” mostly `Gather`, `ScatterND`, `BitShift`, `BitwiseAnd`,
93
+ `Equal`, and `Where`. No node is a custom op. The whole `chip8_cpu.onnx`
94
+ file is ~40 KB.
95
+
96
+ ![Model file structure graph](diagram_structure.png)
97
+
98
+ ## Usage
99
+
100
+ ### Run the bundled demo
101
+
102
+ ```python
103
+ import onnxruntime as ort
104
+ import numpy as np
105
+ from PIL import Image
106
+
107
+ sess = ort.InferenceSession("chip8_snake_demo.onnx",
108
+ providers=["CPUExecutionProvider"])
109
+ frames, = sess.run(None, {}) # no inputs!
110
+
111
+ print(frames.shape, frames.dtype)
112
+ # (90, 32, 64) uint8
113
+
114
+ # Save the final frame
115
+ final = (frames[-1] > 0).astype(np.uint8) * 255
116
+ Image.fromarray(final, mode="L").resize((512, 256)).save("snake_frame.png")
117
+ ```
118
+
119
+ That's the entire usage. No tokenizer, no preprocessing, no postprocessing
120
+ β€” `Run()` returns pixels.
121
+
122
+ ### Load any CHIP-8 ROM into the generic CPU
123
+
124
+ ```python
125
+ import onnxruntime as ort
126
+ import numpy as np
127
+
128
+ sess = ort.InferenceSession("chip8_cpu.onnx",
129
+ providers=["CPUExecutionProvider"])
130
+
131
+ # Initial state
132
+ def initial_ram(rom: bytes) -> np.ndarray:
133
+ FONT = bytes.fromhex("F0909090F02060202070F010F080F0F010F010F0"
134
+ "9090F01010F080F010F0F080F090F0F010204040"
135
+ "F090F090F0F090F010F0F090F09090E090E090E0"
136
+ "F0808080F0E0909090E0F080F080F0F080F08080")
137
+ ram = np.zeros(4096, dtype=np.uint8)
138
+ ram[0x50:0x50+80] = np.frombuffer(FONT, dtype=np.uint8)
139
+ ram[0x200:0x200+len(rom)] = np.frombuffer(rom, dtype=np.uint8)
140
+ return ram
141
+
142
+ regs = np.zeros(40, dtype=np.int32)
143
+ regs[17] = 0x200 # PC
144
+ regs[21] = 0xAB # RNG seed
145
+ ram = initial_ram(open("snake.ch8", "rb").read())
146
+ display = np.zeros(2048, dtype=np.uint8)
147
+ keys = np.zeros(16, dtype=np.uint8)
148
+
149
+ # Run 30 CHIP-8 instructions per tick
150
+ for tick in range(60):
151
+ regs, ram, display = sess.run(None, {
152
+ "regs_in": regs,
153
+ "ram_in": ram,
154
+ "display_in": display,
155
+ "keys": keys,
156
+ "trip_count": np.array(30, dtype=np.int64),
157
+ })
158
+
159
+ # `display` is now a uint8[2048] framebuffer β€” reshape to (32, 64) to view.
160
+ ```
161
+
162
+ A bundled ROM (`snake.ch8`, public domain) is included so you can try this
163
+ straight away.
164
+
165
+ ## Why this exists
166
+
167
+ It's a question about what ONNX *is*. The ONNX operator set, once it grew
168
+ `Loop`, `If`, the `Bitwise*` family (opset 18) and `ScatterND` with
169
+ reduction modes, became Turing-complete in any reasonable sense of the
170
+ phrase. This model demonstrates the consequence: ONNX Runtime, designed
171
+ for evaluating neural networks, can also evaluate **arbitrary
172
+ computations** β€” including a working game console β€” without modification.
173
+
174
+ Concretely the project exists to:
175
+
176
+ * Probe how far the standard ONNX op set actually goes as a general
177
+ computation target.
178
+ * Demonstrate that `Loop` + `Scan output` give you a clean way to express
179
+ *"run a program for N steps, return one tensor per step"* in a single
180
+ `Run()` call.
181
+ * Provide a tiny, complete, self-contained reference for anyone who wants
182
+ to do non-ML things with ONNX.
183
+
184
+ If you want to play CHIP-8 games, there are a hundred better emulators.
185
+ If you want to see what happens when you treat ONNX as a programming
186
+ language, you're in the right place.
187
+
188
+ ## Performance
189
+
190
+ Measured on a Windows ARM64 laptop with ONNX Runtime 1.26 CPU EP, opset 21:
191
+
192
+ | Workload | Throughput |
193
+ |---|---|
194
+ | CHIP-8 instructions per second | ~2,500 |
195
+ | Full snake-title demo (90 frames Γ— 30 ipf = 2,700 instructions) | ~1.1 s |
196
+ | Inner Loop body | ~600 ONNX nodes |
197
+ | Generic CPU model file size | ~40 KB |
198
+ | Snake demo model file size | ~48 KB (includes ROM) |
199
+
200
+ This is plenty fast for CHIP-8 β€” most CHIP-8 games target 500–1000 Hz CPU
201
+ and the model handily exceeds that. ONNX-as-a-CPU is not, however, going
202
+ to be competitive with anything that wants to run a real-time emulator
203
+ properly; per-node overhead in `Loop` bodies dominates everything.
204
+
205
+ ## What's inside the box
206
+
207
+ ```
208
+ .
209
+ β”œβ”€β”€ chip8_cpu.onnx # Generic CHIP-8 CPU (40 KB)
210
+ β”œβ”€β”€ chip8_snake_demo.onnx # Self-contained Snake-title movie (48 KB)
211
+ β”œβ”€β”€ snake.ch8 # Public-domain Snake ROM (1.4 KB)
212
+ β”œβ”€β”€ example_output.gif # What you get when you Run() the demo
213
+ └── README.md # This file
214
+ ```
215
+
216
+ ## License
217
+
218
+ * Code & model files: **MIT**.
219
+ * Bundled `snake.ch8` ROM: **CC0** (from
220
+ [JohnEarnest/chip8Archive](https://github.com/JohnEarnest/chip8Archive)).
221
+
222
+ ## Credits
223
+
224
+ * CHIP-8 was created by Joseph Weisbecker in 1977 for the COSMAC VIP.
225
+ * `snake.ch8` is by John Earnest, CC0.
226
+ * Built with the standard ONNX op set (opset 21) and tested with
227
+ ONNX Runtime 1.26.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
diagram_body.png ADDED

Git LFS Details

  • SHA256: ceae42c7da7932807496783cc0f1996d6eb91c412c2001b705057b403cee11e4
  • Pointer size: 131 Bytes
  • Size of remote file: 351 kB
diagram_structure.png ADDED
diagram_wrapper.png ADDED

Git LFS Details

  • SHA256: acd3bc89bd297ad060a668f76b169521be7e248a8bc7c015da1515570f068815
  • Pointer size: 131 Bytes
  • Size of remote file: 143 kB