yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Jay Kwakdisable ray-tracing-pipeline test (#8023)374026df1

master
14.4 KiB492 linesraw
1// unit-test-record-replay.cpp
2
3#include "../../source/core/slang-http.h"
4#include "../../source/core/slang-io.h"
5#include "../../source/core/slang-process-util.h"
6#include "../../source/core/slang-random-generator.h"
7#include "../../source/core/slang-string-util.h"
8#include "unit-test/slang-unit-test.h"
9
10#include <chrono>
11#include <thread>
12
13using namespace Slang;
14
15static SlangResult createProcess(
16    UnitTestContext* context,
17    const char* processName,
18    const List<String>* optArgs,
19    RefPtr<Process>& outProcess)
20{
21    CommandLine cmdLine;
22    cmdLine.setExecutableLocation(ExecutableLocation(context->executableDirectory, processName));
23    if (optArgs)
24    {
25        cmdLine.m_args.addRange(optArgs->getBuffer(), optArgs->getCount());
26    }
27
28    SLANG_RETURN_ON_FAIL(Process::create(cmdLine, Process::Flag::AttachDebugger, outProcess));
29
30    return SLANG_OK;
31}
32
33struct entryHashInfo
34{
35    int64_t callIdx = -1;
36    int64_t targetIndex = -1;
37    int64_t entryPointIndex = -1;
38    String hash;
39};
40
41static SlangResult parseHashes(List<String> const& lines, List<entryHashInfo>& outHashes)
42{
43    SlangResult res = SLANG_OK;
44
45    for (const auto& line : lines)
46    {
47        List<UnownedStringSlice> tokens;
48        Index skipCharacters = line.indexOf(UnownedStringSlice("[slang-record-replay]:"));
49        if (skipCharacters == -1)
50        {
51            skipCharacters = 0;
52        }
53        else
54        {
55            skipCharacters += strlen("[slang-record-replay]:");
56        }
57        StringUtil::split(UnownedStringSlice(line.getBuffer() + skipCharacters), ',', tokens);
58
59        if (tokens.getCount() != 4)
60        {
61            return SLANG_FAIL;
62        }
63
64        entryHashInfo hashInfo;
65        auto extractToken = [](const UnownedStringSlice& token,
66                               const char splitChar,
67                               UnownedStringSlice& outToken) -> SlangResult
68        {
69            List<UnownedStringSlice> subTokens;
70            StringUtil::split(token, splitChar, subTokens);
71            if (subTokens.getCount() != 2)
72            {
73                return SLANG_FAIL;
74            }
75            outToken = subTokens[1];
76            return SLANG_OK;
77        };
78
79        {
80            UnownedStringSlice subToken;
81            SLANG_RETURN_ON_FAIL(extractToken(tokens[0], ':', subToken));
82            int64_t outNumer = 0;
83            StringUtil::parseInt64(subToken, outNumer);
84            hashInfo.callIdx = outNumer;
85        }
86
87        {
88            UnownedStringSlice subToken;
89            SLANG_RETURN_ON_FAIL(extractToken(tokens[1], ':', subToken));
90            int64_t outNumer = 0;
91            StringUtil::parseInt64(subToken, outNumer);
92            hashInfo.entryPointIndex = outNumer;
93        }
94
95        {
96            UnownedStringSlice subToken;
97            SLANG_RETURN_ON_FAIL(extractToken(tokens[2], ':', subToken));
98            int64_t outNumer = 0;
99            StringUtil::parseInt64(subToken, outNumer);
100            hashInfo.targetIndex = outNumer;
101        }
102
103        {
104            UnownedStringSlice subToken;
105            SLANG_RETURN_ON_FAIL(extractToken(tokens[3], ':', subToken));
106            // remove the white space after ":"
107            hashInfo.hash = subToken.begin() + 1;
108        }
109
110        outHashes.add(hashInfo);
111    }
112    return res;
113}
114
115static int writeEnvironmentVariable(const char* key, const char* val)
116{
117#ifdef _WIN32
118    String var = String(key) + "=" + val;
119    return _putenv(var.getBuffer());
120#else
121    return setenv(key, val, 1);
122#endif
123}
124
125static bool enableRecordLayer()
126{
127    int retCode = writeEnvironmentVariable("SLANG_RECORD_LAYER", "1");
128    return retCode == 0;
129}
130
131static bool disableRecordLayer()
132{
133    int retCode = writeEnvironmentVariable("SLANG_RECORD_LAYER", "0");
134    return retCode == 0;
135}
136
137static bool enableLogInReplayer()
138{
139    int retCode = writeEnvironmentVariable("SLANG_RECORD_LOG_LEVEL", "3");
140    return retCode == 0;
141}
142
143static bool disableLogInReplayer()
144{
145    int retCode = writeEnvironmentVariable("SLANG_RECORD_LOG_LEVEL", "0");
146    return retCode == 0;
147}
148
149static void findRecordFileName(List<String>* fileNames, const String& recordDir)
150{
151    struct Visitor : Path::Visitor
152    {
153        void accept(Path::Type type, const UnownedStringSlice& filename) SLANG_OVERRIDE
154        {
155            if (type == Path::Type::File)
156            {
157                m_fileNames->add(filename);
158            }
159        }
160        Visitor(List<String>* fileNames)
161            : m_fileNames(fileNames)
162        {
163        }
164        List<String>* m_fileNames;
165    };
166
167    Visitor visitor(fileNames);
168    Path::find(recordDir.getBuffer(), "*.cap", &visitor);
169}
170
171static SlangResult launchProcessAndReadStdout(
172    UnitTestContext* context,
173    const List<String>& optArgs,
174    const char* exampleName,
175    RefPtr<Process>& process,
176    ExecuteResult& exeRes)
177{
178    StringBuilder msgBuilder;
179    SlangResult res = createProcess(context, exampleName, &optArgs, process);
180    if (SLANG_FAILED(res))
181    {
182        msgBuilder << "Failed to launch process of '" << exampleName << "'\n";
183        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
184        return res;
185    }
186
187    res = ProcessUtil::readUntilTermination(process, exeRes);
188    if (SLANG_FAILED(res))
189    {
190        msgBuilder << "Failed to read stdout from '" << exampleName << "'\n";
191        msgBuilder << "process ret code: " << exeRes.resultCode;
192        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
193        return res;
194    }
195
196    if (exeRes.resultCode != 0)
197    {
198        msgBuilder << "'" << exampleName << "' exits with failure\n";
199        msgBuilder << "Process ret code: " << exeRes.resultCode << "\n";
200        msgBuilder << "Standard output:\n" << exeRes.standardOutput;
201        msgBuilder << "Standard error:\n" << exeRes.standardError;
202        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
203        return SLANG_FAIL;
204    }
205
206    if (exeRes.standardOutput.getLength() == 0)
207    {
208        msgBuilder << "No stdout found in '" << exampleName << "'\n";
209        msgBuilder << "Standard error: " << exeRes.standardError;
210        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
211        return SLANG_FAIL;
212    }
213    return SLANG_OK;
214}
215
216static SlangResult runExample(
217    UnitTestContext* context,
218    const char* exampleName,
219    const String& recordDir,
220    List<entryHashInfo>& outHashes)
221{
222    SlangResult finalRes = SLANG_OK;
223
224    RefPtr<Process> process;
225    ExecuteResult exeRes;
226    List<String> optArgs;
227    optArgs.add("--test-mode");
228
229    StringBuilder msgBuilder;
230    SlangResult res = SLANG_OK;
231
232    // Set unique record directory for this test
233    writeEnvironmentVariable("SLANG_RECORD_DIRECTORY", recordDir.getBuffer());
234    enableRecordLayer();
235    res = launchProcessAndReadStdout(context, optArgs, exampleName, process, exeRes);
236    disableRecordLayer();
237
238    if (SLANG_FAILED(res))
239    {
240        return res;
241    }
242
243    List<String> hashLines;
244    for (auto line : LineParser(exeRes.standardOutput.getUnownedSlice()))
245    {
246        if (line.getLength() == 0)
247        {
248            continue;
249        }
250
251        if (line.indexOf(UnownedStringSlice("hash:")) == -1)
252        {
253            continue;
254        }
255
256        hashLines.add(line);
257    }
258
259    if (hashLines.getCount() == 0)
260    {
261        msgBuilder << "Hash value is not found for '" << exampleName << "'\n";
262        msgBuilder << "Process ret code: " << exeRes.resultCode << "\n";
263        msgBuilder << "Standard output:\n" << exeRes.standardOutput << "\n";
264        msgBuilder << "Standard error:\n" << exeRes.standardError << "\n";
265        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
266        return SLANG_FAIL;
267    }
268
269    res = parseHashes(hashLines, outHashes);
270    if (SLANG_FAILED(res))
271    {
272        msgBuilder << "Failed to parse hash from stdout of '" << exampleName << "'\n";
273        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
274        return res;
275    }
276
277    return SLANG_OK;
278}
279
280static SlangResult replayExample(
281    UnitTestContext* context,
282    const String& recordDir,
283    List<entryHashInfo>& outHashes)
284{
285    List<String> fileNames;
286    findRecordFileName(&fileNames, recordDir);
287    if (fileNames.getCount() == 0)
288    {
289        getTestReporter()->message(TestMessageType::TestFailure, "No record files found\n");
290        return SLANG_FAIL;
291    }
292
293    List<String> optArgs;
294    String recordFileName = Path::combine(recordDir, fileNames[0]);
295    optArgs.add(recordFileName.getBuffer());
296
297    RefPtr<Process> process;
298    ExecuteResult exeRes;
299
300    StringBuilder msgBuilder;
301    msgBuilder << "replay the test\n";
302
303    enableLogInReplayer();
304    SlangResult res = launchProcessAndReadStdout(context, optArgs, "slang-replay", process, exeRes);
305    disableLogInReplayer();
306
307    if (SLANG_FAILED(res))
308    {
309        return res;
310    }
311
312    List<String> hashLines;
313    for (auto line : LineParser(exeRes.standardOutput.getUnownedSlice()))
314    {
315        if (line.getLength() == 0)
316        {
317            continue;
318        }
319
320        if (line.indexOf(UnownedStringSlice("hash:")) == -1)
321        {
322            continue;
323        }
324
325        hashLines.add(line);
326    }
327
328    res = parseHashes(hashLines, outHashes);
329    if (SLANG_FAILED(res))
330    {
331        msgBuilder << "Failed to parse hash from stdout of 'slang-replay'\n";
332        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
333        return SLANG_FAIL;
334    }
335
336    return SLANG_OK;
337}
338
339static SlangResult resultCompare(
340    List<entryHashInfo> const& expectHashes,
341    List<entryHashInfo> const& resultHashes)
342{
343    if (expectHashes.getCount() == 0)
344    {
345        getTestReporter()->message(TestMessageType::TestFailure, "No hash found\n");
346        return SLANG_FAIL;
347    }
348
349    StringBuilder msgBuilder;
350    if (expectHashes.getCount() != resultHashes.getCount())
351    {
352        msgBuilder << "The number of hashes doesn't match, expect: " << expectHashes.getCount()
353                   << ", actual: " << resultHashes.getCount() << "\n";
354        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
355        return SLANG_FAIL;
356    }
357
358    for (Index i = 0; i < expectHashes.getCount(); i++)
359    {
360        if (expectHashes[i].targetIndex != resultHashes[i].targetIndex)
361        {
362            msgBuilder << "Failed to match 'targetIndex' at index " << i << "\n";
363            msgBuilder << "Expect: " << expectHashes[i].targetIndex
364                       << ", actual: " << resultHashes[i].targetIndex << "\n";
365            getTestReporter()->message(
366                TestMessageType::TestFailure,
367                msgBuilder.toString().getBuffer());
368            return SLANG_FAIL;
369        }
370        if (expectHashes[i].entryPointIndex != resultHashes[i].entryPointIndex)
371        {
372            msgBuilder << "Failed to match 'entryPointIndex' at index " << i << "\n";
373            msgBuilder << "Expect: " << expectHashes[i].entryPointIndex
374                       << ", actual: " << resultHashes[i].entryPointIndex << "\n";
375            getTestReporter()->message(
376                TestMessageType::TestFailure,
377                msgBuilder.toString().getBuffer());
378            return SLANG_FAIL;
379        }
380
381        if (expectHashes[i].hash != resultHashes[i].hash)
382        {
383            msgBuilder << "Failed to match 'hash' at index " << i << "\n";
384            msgBuilder << "Expect: " << expectHashes[i].hash << ", actual: " << resultHashes[i].hash
385                       << "\n";
386            getTestReporter()->message(
387                TestMessageType::TestFailure,
388                msgBuilder.toString().getBuffer());
389            return SLANG_FAIL;
390        }
391    }
392
393    return SLANG_OK;
394}
395
396static SlangResult cleanupRecordFiles(const String& recordDir)
397{
398    SlangResult res = Path::removeNonEmpty(recordDir.getBuffer());
399    if (SLANG_FAILED(res))
400    {
401        StringBuilder msgBuilder;
402        msgBuilder << "Failed to remove '" << recordDir << "' directory\n";
403        getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
404    }
405
406    return res;
407}
408
409static SlangResult runTest(UnitTestContext* context, const char* testName)
410{
411    // Create unique directory for this test to avoid conflicts
412    StringBuilder recordDirBuilder;
413    recordDirBuilder << "slang-record-" << testName;
414    String recordDir = recordDirBuilder.toString();
415
416    List<entryHashInfo> expectHashes;
417    List<entryHashInfo> resultHashes;
418    SlangResult res = SLANG_OK;
419
420    // Run the example to generate recording
421    res = runExample(context, testName, recordDir, expectHashes);
422    if (SLANG_SUCCEEDED(res))
423    {
424        // Replay the recording
425        res = replayExample(context, recordDir, resultHashes);
426        if (SLANG_SUCCEEDED(res))
427        {
428            // Compare results
429            res = resultCompare(expectHashes, resultHashes);
430        }
431    }
432
433    // Always cleanup, regardless of success or failure
434    cleanupRecordFiles(recordDir);
435    return res;
436}
437
438// Those examples all depend on the Vulkan, so we only run them on non-Apple platforms.
439// In the future, we may be able to modify the examples further to remove all the render APIs
440// such that it can be ran on Apple platforms.
441#if !(SLANG_APPLE_FAMILY)
442
443SLANG_UNIT_TEST(RecordReplay_cpu_hello_world)
444{
445    SLANG_CHECK(SLANG_SUCCEEDED(runTest(unitTestContext, "cpu-hello-world")));
446}
447
448SLANG_UNIT_TEST(RecordReplay_triangle)
449{
450    SLANG_CHECK(SLANG_SUCCEEDED(runTest(unitTestContext, "triangle")));
451}
452
453SLANG_UNIT_TEST(RecordReplay_ray_tracing)
454{
455    SLANG_CHECK(SLANG_SUCCEEDED(runTest(unitTestContext, "ray-tracing")));
456}
457
458// This causes a Windows Graphics driver crash.
459// Temporarily disabled; issue #8022
460#if 0
461SLANG_UNIT_TEST(RecordReplay_ray_tracing_pipeline)
462{
463    SLANG_CHECK(SLANG_SUCCEEDED(runTest(unitTestContext, "ray-tracing-pipeline")));
464}
465#endif
466
467SLANG_UNIT_TEST(RecordReplay_autodiff_texture)
468{
469    SLANG_CHECK(SLANG_SUCCEEDED(runTest(unitTestContext, "autodiff-texture")));
470}
471
472SLANG_UNIT_TEST(RecordReplay_gpu_printing)
473{
474    SLANG_CHECK(SLANG_SUCCEEDED(runTest(unitTestContext, "gpu-printing")));
475}
476
477#if 0
478// These examples requires reflection API to replay, we have to disable
479// it for now. "model-viewer",
480
481SLANG_UNIT_TEST(RecordReplay_shader_object)
482{
483    SLANG_CHECK(SLANG_SUCCEEDED(runTest(unitTestContext, "shader-object")));
484}
485
486SLANG_UNIT_TEST(RecordReplay_model_viewer)
487{
488    SLANG_CHECK(SLANG_SUCCEEDED(runTest(unitTestContext, "model-viewer")));
489}
490#endif
491
492#endif