1 /*---------------------------------------------------------------------------*
2 Project: Horizon
3 File: FsSampleStreamingFrame.cpp
4
5 Copyright (C)2009-2012 Nintendo Co., Ltd. All rights reserved.
6
7 These coded instructions, statements, and computer programs contain
8 proprietary information of Nintendo of America Inc. and/or Nintendo
9 Company Ltd., and are protected by Federal copyright law. They may
10 not be disclosed to third parties or copied or duplicated in any form,
11 in whole or in part, without the prior written consent of Nintendo.
12
13 $Rev: 46547 $
14 *---------------------------------------------------------------------------*/
15
16 #include <nn.h>
17 #include "demo.h"
18
19 #include "../Common/FsSampleCommon.h"
20 #include "FsSampleStreaming.h"
21
22 using namespace nn;
23 namespace sample { namespace fs { namespace streaming {
24
25 enum ENUM_TASK
26 {
27 BG_TASK_NONE,
28 BG_TASK_SAVE,
29 TASK_REVERT,
30 BG_TASK_LOAD,
31
32 ENUM_TASK_NUM
33 } s_BgTask;
34
35 enum ENUM_REC_STATE
36 {
37 REC_STATE_NONE,
38 REC_STATE_RUNNING,
39 REC_STATE_SUCCEEDED,
40 REC_STATE_FAILED,
41
42 ENUM_REC_STATE_NUM
43 } s_RecState;
44
45 enum ENUM_PLAY_STATE
46 {
47 PLAY_STATE_NONE,
48 PLAY_STATE_RUNNING,
49 PLAY_STATE_SUCCEEDED,
50 PLAY_STATE_STOPPED,
51 PLAY_STATE_FAILED,
52
53 ENUM_PLAY_STATE_NUM
54 } s_PlayState;
55
56 enum ENUM_BG_STATE
57 {
58 STATE_BG_NONE,
59 STATE_BG_SAVE,
60 STATE_BG_SAVE_SUCCEEDED,
61 STATE_BG_SAVE_FAILED,
62 STATE_BG_LOAD,
63 STATE_BG_LOAD_SUCCEEDED,
64 STATE_BG_LOAD_FAILED,
65
66 ENUM_BG_STATE_NUM
67 } s_BgState;
68
69 bool s_IsWorkerThreadAlive;
70 nn::os::Event s_WorkerEvent;
71 nn::os::Thread s_WorkerThread;
72
73 int s_RecordedFrameNum = 0;
74 int s_DroppedFrameNum = 0;
75
76 int s_BgSaveData = 0;
77 int s_LoadFileNum;
78 int s_LoadFileIndex;
79
80 bit8 s_MemoBuffer[MEMO_BUFFER_SIZE];
81
82 static nn::hid::CTR::TouchPanelStatus s_TouchPanelStatus;
83 static nn::hid::CTR::TouchPanelStatus s_PreviousTouchPanel;
84
85 GLuint s_TextureId = 0;
86
DrawPoint(const u16 orgX,const u16 orgY)87 void DrawPoint(const u16 orgX, const u16 orgY)
88 {
89 if (MEMO_X <= orgX && orgX < MEMO_X + MEMO_WIDTH &&
90 MEMO_Y <= orgY && orgY < MEMO_Y + MEMO_HEIGHT)
91 {
92 // When mapping the memo content to a texture, you must convert from a linear format to a block format.
93 // With this sample, this conversion process is performed in advance when there is touch input, and the converted data is used internally.
94 u16 x = orgX - MEMO_X;
95 u16 y = orgY - MEMO_Y;
96 u32 id = y / TEXTURE_BLOCK_SIZE * MEMO_WIDTH * TEXTURE_BLOCK_SIZE + x / TEXTURE_BLOCK_SIZE * TEXTURE_BLOCK_SIZE * TEXTURE_BLOCK_SIZE +
97 (((x >> 0) & 1) << 0) +
98 (((x >> 1) & 1) << 2) +
99 (((x >> 2) & 1) << 4) +
100 (((y >> 0) & 1) << 1) +
101 (((y >> 1) & 1) << 3) +
102 (((y >> 2) & 1) << 5);
103
104 s_MemoBuffer[id / 8] |= 1 << (id % 8);
105 }
106 }
107
DrawLine(const nn::hid::CTR::TouchPanelStatus & now,const nn::hid::CTR::TouchPanelStatus & previous)108 void DrawLine(const nn::hid::CTR::TouchPanelStatus &now, const nn::hid::CTR::TouchPanelStatus &previous)
109 {
110 if (previous.touch)
111 {
112 s32 diffX = now.x - previous.x;
113 s32 diffY = now.y - previous.y;
114 s32 absDiffX = nn::math::Abs(diffX);
115 s32 absDiffY = nn::math::Abs(diffY);
116
117 if (diffX == 0 && diffY == 0)
118 {
119 DrawPoint(now.x, now.y);
120 }
121 else if (absDiffX < absDiffY)
122 {
123 for (int i = 0; i < absDiffY; ++i)
124 {
125 DrawPoint(previous.x + i * diffX / absDiffY, previous.y + i * diffY / absDiffY);
126 }
127 }
128 else
129 {
130 for (int i = 0; i < absDiffX; ++i)
131 {
132 DrawPoint(previous.x + i * diffX / absDiffX, previous.y + i * diffY / absDiffX);
133 }
134 }
135 }
136 }
137
UpdateTexture(demo::RenderSystemDrawing & renderSystem,const bit8 * src)138 void UpdateTexture(demo::RenderSystemDrawing &renderSystem, const bit8 *src)
139 {
140 const int PIXEL_NUM_PER_BLOCK = TEXTURE_BLOCK_SIZE * TEXTURE_BLOCK_SIZE;
141 const int HORIZONTAL_BLOCK_NUM = MEMO_WIDTH / TEXTURE_BLOCK_SIZE;
142 const int VERTICAL_BLOCK_NUM = MEMO_HEIGHT / TEXTURE_BLOCK_SIZE;
143
144 u8* textureDataBuffer = new bit8[4 * MEMO_TEXTURE_WIDTH * MEMO_TEXTURE_HEIGHT];
145 bit8 data;
146 for (int j = 0; j < VERTICAL_BLOCK_NUM; ++j)
147 {
148 u32* a = (u32*)(textureDataBuffer + (j + (MEMO_TEXTURE_HEIGHT - MEMO_HEIGHT) / TEXTURE_BLOCK_SIZE) * 4 * MEMO_TEXTURE_WIDTH / TEXTURE_BLOCK_SIZE * PIXEL_NUM_PER_BLOCK);
149 for (int i = 0; i < HORIZONTAL_BLOCK_NUM; ++i)
150 {
151 for (int k = 0; k < PIXEL_NUM_PER_BLOCK; ++k)
152 {
153 if (k % 8 == 0)
154 {
155 data = *src;
156 src++;
157 }
158 *a = (data & 1) ? 0 : 0xFFFFFFFF;
159 a++;
160 data >>= 1;
161 }
162 }
163 }
164
165 if (s_TextureId != 0)
166 {
167 NN_PANIC_IF_FALSE(renderSystem.DeleteTexture(s_TextureId));
168 }
169
170 GLenum format = GL_RGBA_NATIVE_DMP;
171 GLenum target = GL_TEXTURE_2D;
172 GLenum internalFormat = GL_RGBA_NATIVE_DMP;
173 GLenum type = GL_UNSIGNED_BYTE;
174 GLuint textureId = 0;
175
176 renderSystem.GenerateTexture(target,
177 internalFormat, MEMO_TEXTURE_WIDTH, MEMO_TEXTURE_HEIGHT,
178 format, type, textureDataBuffer, textureId);
179
180 delete[] textureDataBuffer;
181
182 if ( textureId != 0 )
183 {
184 s_TextureId = textureId;
185 }
186 }
187
DrawMemo(demo::RenderSystemDrawing & renderSystem,const bit8 * src)188 void DrawMemo(demo::RenderSystemDrawing &renderSystem, const bit8 *src)
189 {
190 UpdateTexture(renderSystem, src);
191
192 renderSystem.FillTexturedRectangle(s_TextureId,
193 MEMO_X, MEMO_Y,
194 MEMO_WIDTH, MEMO_HEIGHT,
195 MEMO_WIDTH, MEMO_HEIGHT,
196 512, 256);
197 }
198
ProceedDisplay0(demo::RenderSystemDrawing & renderSystem)199 void ProceedDisplay0(demo::RenderSystemDrawing &renderSystem)
200 {
201 renderSystem.SetRenderTarget(NN_GX_DISPLAY0);
202 renderSystem.SetColor(1.0f, 1.0f, 1.0f);
203 renderSystem.Clear();
204
205 switch (s_RecState)
206 {
207 case REC_STATE_RUNNING:
208 renderSystem.DrawText(0, LINE_HEIGHT * 1, "Recording to %d", s_LoadFileNum);
209 break;
210 case REC_STATE_SUCCEEDED:
211 renderSystem.DrawText(0, LINE_HEIGHT * 1, "Recording Succeeded");
212 break;
213 case REC_STATE_FAILED:
214 renderSystem.DrawText(0, LINE_HEIGHT * 1, "Recording Failed");
215 break;
216 default:
217 break;
218 }
219
220 switch(s_PlayState)
221 {
222 case PLAY_STATE_RUNNING:
223 renderSystem.DrawText(0, LINE_HEIGHT * 2, "Playing %d", s_LoadFileIndex);
224 break;
225 case PLAY_STATE_SUCCEEDED:
226 renderSystem.DrawText(0, LINE_HEIGHT * 2, "Playing Succeeded");
227 break;
228 case PLAY_STATE_STOPPED:
229 renderSystem.DrawText(0, LINE_HEIGHT * 2, "Playing Stopped");
230 break;
231 case PLAY_STATE_FAILED:
232 renderSystem.DrawText(0, LINE_HEIGHT * 2, "Playing Failed");
233 break;
234 default:
235 break;
236 }
237
238 switch (s_BgState)
239 {
240 case STATE_BG_SAVE:
241 renderSystem.DrawText(0, LINE_HEIGHT * 5, "Saving...");
242 break;
243 case STATE_BG_SAVE_SUCCEEDED:
244 renderSystem.DrawText(0, LINE_HEIGHT * 5, "Saving Succeeded.");
245 break;
246 case STATE_BG_SAVE_FAILED:
247 renderSystem.DrawText(0, LINE_HEIGHT * 5, "Saving Failed.");
248 break;
249 case STATE_BG_LOAD:
250 renderSystem.DrawText(0, LINE_HEIGHT * 5, "Loading...");
251 break;
252 case STATE_BG_LOAD_SUCCEEDED:
253 renderSystem.DrawText(0, LINE_HEIGHT * 5, "Loading Succeeded.");
254 break;
255 case STATE_BG_LOAD_FAILED:
256 renderSystem.DrawText(0, LINE_HEIGHT * 5, "Loading Failed.");
257 break;
258 default:
259 break;
260 }
261
262 renderSystem.DrawText(0, LINE_HEIGHT * 11, "A: %s Movie %d", (s_RecState == REC_STATE_RUNNING) ? "Stop" : "Record", s_LoadFileNum);
263 if (s_LoadFileNum > 0)
264 {
265 renderSystem.DrawText(0, LINE_HEIGHT * 12, "B: %s Movie %d", (s_PlayState == PLAY_STATE_RUNNING) ? "Stop" : "Play", s_LoadFileIndex);
266 renderSystem.DrawText(0, LINE_HEIGHT * 13, "Up & Down: Change movie No.");
267 }
268
269 renderSystem.DrawText(0, LINE_HEIGHT * 15, "L: Clear buffer");
270
271 renderSystem.DrawText(0, LINE_HEIGHT * 18, "data: %08X", s_BgSaveData);
272
273 renderSystem.DrawText(0, LINE_HEIGHT * 20, "X: Save data in background");
274 renderSystem.DrawText(0, LINE_HEIGHT * 21, "Y: Load data in background");
275 renderSystem.DrawText(0, LINE_HEIGHT * 22, "Left & Right: Change data");
276
277
278 renderSystem.DrawText(100, LINE_HEIGHT * 25, "Recorded frame:%8d", s_RecordedFrameNum);
279 renderSystem.DrawText(100, LINE_HEIGHT * 26, "Dropped frame: %8d", s_DroppedFrameNum);
280
281
282 renderSystem.SwapBuffers();
283
284 }
285
ProceedDisplay1(demo::RenderSystemDrawing & renderSystem)286 void ProceedDisplay1(demo::RenderSystemDrawing &renderSystem)
287 {
288 renderSystem.SetRenderTarget(NN_GX_DISPLAY1);
289 renderSystem.Clear();
290 DrawMemo(renderSystem, s_MemoBuffer);
291
292 renderSystem.SwapBuffers();
293 }
294
ProceedDisplay(demo::RenderSystemDrawing & renderSystem)295 void ProceedDisplay(demo::RenderSystemDrawing &renderSystem)
296 {
297 ProceedDisplay0(renderSystem);
298 ProceedDisplay1(renderSystem);
299 }
300
CallStartRecording()301 void CallStartRecording()
302 {
303 wchar_t fileName[256];
304 nstd::TSNPrintf(fileName, sizeof(fileName) / sizeof(fileName[0]), L"%ls%04d.dat",MEMO_FILE_NAME, s_LoadFileNum);
305 Result result = StartRecording(fileName);
306 if (result.IsSuccess())
307 {
308 s_RecordedFrameNum = 0;
309 s_DroppedFrameNum = 0;
310 s_RecState = REC_STATE_RUNNING;
311 }
312 else
313 {
314 NN_DBG_PRINT_RESULT(result);
315 s_RecState = REC_STATE_FAILED;
316 }
317 }
318
CallStopRecording()319 void CallStopRecording()
320 {
321 Result result = StopRecording();
322 if (result.IsSuccess())
323 {
324 s_LoadFileNum++;
325 s_RecState = REC_STATE_SUCCEEDED;
326 }
327 else
328 {
329 NN_DBG_PRINT_RESULT(result);
330 s_RecState = REC_STATE_FAILED;
331 }
332 }
333
CallStartPlaying()334 void CallStartPlaying()
335 {
336 wchar_t fileName[256];
337 nstd::TSNPrintf(fileName, sizeof(fileName) / sizeof(fileName[0]), L"%ls%04d.dat",MEMO_FILE_NAME, s_LoadFileIndex);
338 Result result = StartPlaying(fileName);
339 if (result.IsSuccess())
340 {
341 s_PlayState = PLAY_STATE_RUNNING;
342 }
343 else
344 {
345 NN_DBG_PRINT_RESULT(result);
346 s_PlayState = PLAY_STATE_FAILED;
347 }
348 }
349
CallStopPlaying()350 void CallStopPlaying()
351 {
352 Result result = StopPlaying();
353 if (result.IsSuccess())
354 {
355 s_PlayState = PLAY_STATE_STOPPED;
356 }
357 else
358 {
359 NN_DBG_PRINT_RESULT(result);
360 s_PlayState = PLAY_STATE_FAILED;
361 }
362 }
363
ProceedMainTask()364 void ProceedMainTask()
365 {
366 if (s_PlayState == PLAY_STATE_RUNNING)
367 {
368 PacketData *packet;
369 if (!LoadMemo(&packet))
370 {
371 // Does nothing when there is no loaded data.
372 }
373 else if (packet)
374 {
375 std::memcpy(s_MemoBuffer, packet->GetData(), packet->GetSize());
376 packet->Free();
377 }
378 else
379 {
380 NN_ERR_THROW_FATAL_ALL(FinishPlaying());
381 s_PlayState = PLAY_STATE_SUCCEEDED;
382 }
383 }
384
385 if (s_TouchPanelStatus.touch)
386 {
387 DrawLine(s_TouchPanelStatus, s_PreviousTouchPanel);
388 }
389
390 if (s_RecState == REC_STATE_RUNNING)
391 {
392 ENUM_RECORDING_RESULT result = SaveMemo(s_MemoBuffer);
393 if (result == RECORDING_RESULT_BUFFER_FULL)
394 {
395 s_DroppedFrameNum++;
396 }
397 else if (result == RECORDING_RESULT_FILE_FULL)
398 {
399 CallStopRecording();
400 }
401 else
402 {
403 s_RecordedFrameNum++;
404 }
405 }
406 }
407
WorkerThread()408 void WorkerThread()
409 {
410 while (s_IsWorkerThreadAlive)
411 {
412 s_WorkerEvent.Wait();
413 switch (s_BgTask)
414 {
415 case BG_TASK_SAVE:
416 s_BgState = STATE_BG_SAVE;
417 s_BgState = (BgSave(s_BgSaveData).IsSuccess()) ? STATE_BG_SAVE_SUCCEEDED : STATE_BG_SAVE_FAILED;
418 break;
419 case BG_TASK_LOAD:
420 s_BgState = STATE_BG_LOAD;
421 s_BgState = (BgLoad(s_BgSaveData).IsSuccess()) ? STATE_BG_LOAD_SUCCEEDED : STATE_BG_LOAD_FAILED;
422 break;
423 default:
424 s_BgState = STATE_BG_NONE;
425 break;
426 }
427
428 s_BgTask = BG_TASK_NONE;
429 }
430 }
431
ProceedHid()432 void ProceedHid()
433 {
434 static nn::hid::CTR::PadReader padReader;
435 static nn::hid::CTR::PadStatus padStatus;
436 static nn::hid::CTR::TouchPanelReader touchPanelReader;
437
438 if (!padReader.ReadLatest(&padStatus))
439 {
440 nn::dbg::detail::TPrintf("Getting pad status failed.\n");
441 }
442
443 if (padStatus.trigger & nn::hid::BUTTON_A)
444 {
445 if (s_RecState != REC_STATE_RUNNING)
446 {
447 CallStartRecording();
448 }
449 else if (s_RecState == REC_STATE_RUNNING)
450 {
451 CallStopRecording();
452 }
453 }
454 else if (padStatus.trigger & nn::hid::BUTTON_B)
455 {
456 if (s_PlayState != PLAY_STATE_RUNNING)
457 {
458 CallStartPlaying();
459 }
460 else if (s_PlayState == PLAY_STATE_RUNNING)
461 {
462 CallStopPlaying();
463 }
464 }
465 else if ((padStatus.trigger & nn::hid::BUTTON_X) && s_BgTask == BG_TASK_NONE)
466 {
467 s_BgTask = BG_TASK_SAVE;
468 s_WorkerEvent.Signal();
469 }
470 else if ((padStatus.trigger & nn::hid::BUTTON_Y) && s_BgTask == BG_TASK_NONE)
471 {
472 s_BgTask = BG_TASK_LOAD;
473 s_WorkerEvent.Signal();
474 }
475 else if (padStatus.trigger & nn::hid::BUTTON_L)
476 {
477 std::memset(s_MemoBuffer, 0, MEMO_BUFFER_SIZE);
478 }
479 else if (padStatus.trigger & nn::hid::BUTTON_LEFT)
480 {
481 s_BgSaveData--;
482 }
483 else if (padStatus.trigger & nn::hid::BUTTON_RIGHT)
484 {
485 s_BgSaveData++;
486 }
487 else if (padStatus.trigger & nn::hid::BUTTON_UP && s_LoadFileNum > 0 && s_PlayState != PLAY_STATE_RUNNING)
488 {
489 s_LoadFileIndex = (s_LoadFileIndex + 1) % s_LoadFileNum;
490 }
491 else if (padStatus.trigger & nn::hid::BUTTON_DOWN && s_LoadFileNum > 0 && s_PlayState != PLAY_STATE_RUNNING)
492 {
493 s_LoadFileIndex = (s_LoadFileIndex - 1 + s_LoadFileNum) % s_LoadFileNum;
494 }
495
496 s_PreviousTouchPanel = s_TouchPanelStatus;
497 if (!touchPanelReader.ReadLatest(&s_TouchPanelStatus))
498 {
499 nn::dbg::detail::TPrintf("Getting touch panel status failed.\n");
500 }
501 }
502
503 // Initializes the save data.
FormatSaveData()504 void FormatSaveData()
505 {
506 const size_t maxFiles = 8;
507 const size_t maxDirectories = 8;
508 const bool isDuplicateAll = true;
509
510 // Format the save data region
511 Result result = nn::fs::FormatSaveData(maxFiles, maxDirectories, isDuplicateAll);
512 if (result.IsFailure())
513 {
514 NN_LOG("FormatSaveData() failed.\n");
515 NN_ERR_THROW_FATAL_ALL(result);
516 }
517
518 // Mount in order to compose the data default state
519 result = nn::fs::MountSaveData(SAVE_DATA_ARCHIVE);
520 if (result.IsFailure())
521 {
522 NN_LOG("MountSaveData() failed : Cannot use save data!\n");
523 NN_ERR_THROW_FATAL_ALL(result);
524 }
525
526 // Create each file
527 nn::fs::CreateFile(DATA_FILE_NAME, sizeof(SaveData));
528
529 // Commit
530 NN_ERR_THROW_FATAL_ALL(nn::fs::CommitSaveData(SAVE_DATA_ARCHIVE));
531
532 // Unmount
533 nn::fs::Unmount(SAVE_DATA_ARCHIVE);
534 }
535
536 // Save data check when starting the application
InitializeSaveData()537 void InitializeSaveData()
538 {
539 Result result = nn::fs::MountSaveData(SAVE_DATA_ARCHIVE);
540 if (result.IsFailure())
541 {
542 NN_DBG_PRINT_RESULT(result);
543
544 if ((result <= nn::fs::ResultNotFormatted()) ||
545 (result <= nn::fs::ResultBadFormat()) ||
546 (result <= nn::fs::ResultVerificationFailed()))
547 {
548 // Save data needs to be initialized
549 FormatSaveData();
550 }
551 else
552 {
553 // Unexpected errors
554 NN_ERR_THROW_FATAL_ALL(result);
555 }
556 }
557 else
558 {
559 nn::fs::Unmount(SAVE_DATA_ARCHIVE);
560 }
561 }
562
563 // Expanded save data check when starting the application
InitializeExtSaveData()564 void InitializeExtSaveData()
565 {
566 Result result = nn::fs::MountExtSaveData(EXT_ARCHIVE, EXT_SAVEDATA_ID);
567 if (result.IsFailure())
568 {
569 NN_DBG_PRINT_RESULT(result);
570
571 if ((result <= nn::fs::ResultNotFound()) ||
572 (result <= nn::fs::ResultNotFormatted()) ||
573 (result <= nn::fs::ResultBadFormat()) ||
574 (result <= nn::fs::ResultVerificationFailed()))
575 {
576 nn::fs::DeleteExtSaveData(EXT_SAVEDATA_ID);
577 bit8 iconData[64];
578 NN_ERR_THROW_FATAL_ALL(nn::fs::CreateExtSaveData(EXT_SAVEDATA_ID, iconData, sizeof(iconData), 0, MEMO_FILE_NUM_MAX));
579 NN_ERR_THROW_FATAL_ALL(nn::fs::MountExtSaveData(EXT_ARCHIVE, EXT_SAVEDATA_ID));
580 }
581 else
582 {
583 // Unexpected errors
584 NN_ERR_THROW_FATAL_ALL(result);
585 }
586 }
587 }
588
589 } // namespace streaming
590
591 using namespace streaming;
592
MainLoop(demo::RenderSystemDrawing & renderSystem)593 void MainLoop(demo::RenderSystemDrawing &renderSystem)
594 {
595 ProceedHid();
596 ProceedMainTask();
597 ProceedDisplay(renderSystem);
598
599 nngxWaitVSync(NN_GX_DISPLAY_BOTH);
600 }
601
Initialize(demo::RenderSystemDrawing & renderSystem)602 void Initialize(demo::RenderSystemDrawing &renderSystem)
603 {
604 nn::fs::Initialize();
605 InitializeSaveData();
606 InitializeExtSaveData();
607
608 nn::fs::SetAnalysisLog(true);
609 renderSystem.SetClearColor(NN_GX_DISPLAY1, 0.5f, 0.5f, 0.5f, 0.0f);
610
611 s_LoadFileNum = 0;
612 s_LoadFileIndex = 0;
613 while (true)
614 {
615 wchar_t fileName[256];
616 nstd::TSNPrintf(fileName, sizeof(fileName) / sizeof(fileName[0]), L"%ls%04d.dat",MEMO_FILE_NAME, s_LoadFileNum);
617
618 nn::fs::FileInputStream file;
619 Result result = file.TryInitialize(fileName);
620 if (result.IsFailure())
621 {
622 break;
623 }
624 s_LoadFileNum++;
625 }
626
627 s_WorkerEvent.Initialize(false);
628 s_IsWorkerThreadAlive = true;
629 NN_ERR_THROW_FATAL_ALL(s_WorkerThread.TryStartUsingAutoStack(&WorkerThread, 4 * 1024, nn::os::Thread::GetCurrentPriority() + 1));
630 }
631
Finalize()632 void Finalize()
633 {
634 s_IsWorkerThreadAlive = false;
635 s_WorkerEvent.Signal();
636 s_WorkerThread.Join();
637 }
638
639 }} // namespace sample::fs
640