Simplified SIMD dispatch wrappers
This commit is contained in:
parent
75073cf8c0
commit
c0fac2cfbb
4 changed files with 277 additions and 129 deletions
|
|
@ -5,236 +5,361 @@
|
|||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#include "SIMDHelpers.h"
|
||||
#include <array>
|
||||
#include "cpuid/cpuinfo.hpp"
|
||||
#include "SIMDConfig.h"
|
||||
#include "Debug.h"
|
||||
#include "simd/HelpersSSE.h"
|
||||
#include "simd/HelpersAVX.h"
|
||||
#include "cpuid/cpuinfo.hpp"
|
||||
#include <array>
|
||||
#include <mutex>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
static std::array<bool, static_cast<unsigned>(SIMDOps::_sentinel)> simdStatus;
|
||||
static bool simdStatusInitialized = false;
|
||||
static cpuid::cpuinfo cpuInfo;
|
||||
template <class T>
|
||||
struct SIMDDispatch {
|
||||
constexpr SIMDDispatch() = default;
|
||||
|
||||
void resetSIMDStatus()
|
||||
void resetStatus();
|
||||
bool getStatus(SIMDOps op) const;
|
||||
void setStatus(SIMDOps op, bool enable);
|
||||
|
||||
void (*writeInterleaved)(const T* inputLeft, const T* inputRight, T* output, unsigned outputSize) noexcept = &writeInterleavedScalar<float>;
|
||||
void (*readInterleaved)(const T* input, T* outputLeft, T* outputRight, unsigned inputSize) noexcept = &readInterleavedScalar<float>;
|
||||
void (*applyGain)(const T* gain, const T* input, T* output, unsigned size) noexcept = &applyGainScalar<float>;
|
||||
void (*applyGain1)(T gain, const T* input, T* output, unsigned size) noexcept = &applyGainScalar<float>;
|
||||
void (*divide)(const T* input, const T* divisor, T* output, unsigned size) noexcept = ÷Scalar<float>;
|
||||
void (*multiplyAdd)(const T* gain, const T* input, T* output, unsigned size) noexcept = &multiplyAddScalar<float>;
|
||||
void (*multiplyAdd1)(T gain, const T* input, T* output, unsigned size) noexcept = &multiplyAddScalar<float>;
|
||||
T (*linearRamp)(T* output, T start, T step, unsigned size) noexcept = &linearRampScalar<float>;
|
||||
T (*multiplicativeRamp)(T* output, T start, T step, unsigned size) noexcept = &multiplicativeRampScalar<float>;
|
||||
void (*add)(const T* input, T* output, unsigned size) noexcept = &addScalar<float>;
|
||||
void (*add1)(T value, T* output, unsigned size) noexcept = &addScalar<float>;
|
||||
void (*subtract)(const T* input, T* output, unsigned size) noexcept = &subtractScalar<float>;
|
||||
void (*subtract1)(T value, T* output, unsigned size) noexcept = &subtractScalar<float>;
|
||||
void (*copy)(const T* input, T* output, unsigned size) noexcept = ©Scalar<float>;
|
||||
void (*cumsum)(const T* input, T* output, unsigned size) noexcept = &cumsumScalar<float>;
|
||||
void (*diff)(const T* input, T* output, unsigned size) noexcept = &diffScalar<float>;
|
||||
T (*mean)(const T* vector, unsigned size) noexcept = &meanScalar<float>;
|
||||
T (*meanSquared)(const T* vector, unsigned size) noexcept = &meanSquaredScalar<float>;
|
||||
|
||||
private:
|
||||
std::array<bool, static_cast<unsigned>(SIMDOps::_sentinel)> simdStatus;
|
||||
};
|
||||
|
||||
///
|
||||
|
||||
static SIMDDispatch<float> simdDispatch;
|
||||
|
||||
void resetSIMDOpStatus()
|
||||
{
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::writeInterleaved)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::readInterleaved)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::fill)] = true;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::gain)] = true;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::divide)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::linearRamp)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::multiplicativeRamp)] = true;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::add)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::subtract)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::multiplyAdd)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::copy)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::cumsum)] = true;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::diff)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::sfzInterpolationCast)] = true;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::mean)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::meanSquared)] = false;
|
||||
simdStatus[static_cast<unsigned>(SIMDOps::upsampling)] = true;
|
||||
simdStatusInitialized = true;
|
||||
simdDispatch.resetStatus();
|
||||
}
|
||||
|
||||
void setSIMDOpStatus(SIMDOps op, bool status)
|
||||
{
|
||||
if (!simdStatusInitialized)
|
||||
resetSIMDStatus();
|
||||
|
||||
simdStatus[static_cast<unsigned>(op)] = status;
|
||||
simdDispatch.setStatus(op, status);
|
||||
}
|
||||
|
||||
bool getSIMDOpStatus(SIMDOps op)
|
||||
{
|
||||
if (!simdStatusInitialized)
|
||||
resetSIMDStatus();
|
||||
|
||||
return simdStatus[static_cast<unsigned>(op)];
|
||||
return simdDispatch.getStatus(op);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
void readInterleaved(const float* input, float* outputLeft, float* outputRight, unsigned inputSize) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::readInterleaved)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return readInterleavedSSE(input, outputLeft, outputRight, inputSize);
|
||||
}
|
||||
return readInterleavedScalar(input, outputLeft, outputRight, inputSize);
|
||||
return simdDispatch.readInterleaved(input, outputLeft, outputRight, inputSize);
|
||||
}
|
||||
|
||||
void writeInterleaved(const float* inputLeft, const float* inputRight, float* output, unsigned outputSize) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::writeInterleaved)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return writeInterleavedSSE(inputLeft, inputRight, output, outputSize);
|
||||
}
|
||||
return writeInterleavedScalar(inputLeft, inputRight, output, outputSize);
|
||||
return simdDispatch.writeInterleaved(inputLeft, inputRight, output, outputSize);
|
||||
}
|
||||
|
||||
template <>
|
||||
void applyGain<float>(float gain, const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::gain)) {
|
||||
if (cpuInfo.has_avx())
|
||||
return applyGainAVX(gain, input, output, size);
|
||||
else if (cpuInfo.has_sse())
|
||||
return applyGainSSE(gain, input, output, size);
|
||||
}
|
||||
return applyGainScalar(gain, input, output, size);
|
||||
return simdDispatch.applyGain1(gain, input, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void applyGain<float>(const float* gain, const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::gain)) {
|
||||
if (cpuInfo.has_avx())
|
||||
return applyGainAVX(gain, input, output, size);
|
||||
else if (cpuInfo.has_sse())
|
||||
return applyGainSSE(gain, input, output, size);
|
||||
}
|
||||
return applyGainScalar(gain, input, output, size);
|
||||
return simdDispatch.applyGain(gain, input, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void divide<float>(const float* input, const float* divisor, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::divide)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return divideSSE(input, divisor, output, size);
|
||||
}
|
||||
return divideScalar(input, divisor, output, size);
|
||||
return simdDispatch.divide(input, divisor, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void multiplyAdd<float>(const float* gain, const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::multiplyAdd)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return multiplyAddSSE(gain, input, output, size);
|
||||
}
|
||||
return multiplyAddScalar(gain, input, output, size);
|
||||
return simdDispatch.multiplyAdd(gain, input, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void multiplyAdd<float>(float gain, const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::multiplyAdd)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return multiplyAddSSE(gain, input, output, size);
|
||||
}
|
||||
return multiplyAddScalar(gain, input, output, size);
|
||||
return simdDispatch.multiplyAdd1(gain, input, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
float linearRamp<float>(float* output, float start, float step, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::linearRamp)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return linearRampSSE(output, start, step, size);
|
||||
}
|
||||
return linearRampScalar(output, start, step, size);
|
||||
return simdDispatch.linearRamp(output, start, step, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
float multiplicativeRamp<float>(float* output, float start, float step, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::multiplicativeRamp)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return multiplicativeRampSSE(output, start, step, size);
|
||||
}
|
||||
return multiplicativeRampScalar(output, start, step, size);
|
||||
return simdDispatch.multiplicativeRamp(output, start, step, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void add<float>(const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::add)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return addSSE(input, output, size);
|
||||
}
|
||||
return addScalar(input, output, size);
|
||||
return simdDispatch.add(input, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void add<float>(float value, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::add)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return addSSE(value, output, size);
|
||||
}
|
||||
return addScalar(value, output, size);
|
||||
return simdDispatch.add1(value, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void subtract<float>(const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::subtract)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return subtractSSE(input, output, size);
|
||||
}
|
||||
return subtractScalar(input, output, size);
|
||||
return simdDispatch.subtract(input, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void subtract<float>(float value, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::subtract)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return subtractSSE(value, output, size);
|
||||
}
|
||||
return subtractScalar(value, output, size);
|
||||
return simdDispatch.subtract1(value, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void copy<float>(const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::copy)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return copySSE(input, output, size);
|
||||
}
|
||||
std::copy(input, input + size, output);
|
||||
return simdDispatch.copy(input, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
float mean<float>(const float* vector, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::mean)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return meanSSE(vector, size);
|
||||
}
|
||||
return meanScalar(vector, size);
|
||||
return simdDispatch.mean(vector, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
float meanSquared<float>(const float* vector, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::meanSquared)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return meanSquaredSSE(vector, size);
|
||||
}
|
||||
return meanSquaredScalar(vector, size);
|
||||
return simdDispatch.meanSquared(vector, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void cumsum<float>(const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::cumsum)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return cumsumSSE(input, output, size);
|
||||
}
|
||||
return cumsumScalar(input, output, size);
|
||||
return simdDispatch.cumsum(input, output, size);
|
||||
}
|
||||
|
||||
template <>
|
||||
void diff<float>(const float* input, float* output, unsigned size) noexcept
|
||||
{
|
||||
if (getSIMDOpStatus(SIMDOps::diff)) {
|
||||
if (cpuInfo.has_sse())
|
||||
return diffSSE(input, output, size);
|
||||
return simdDispatch.diff(input, output, size);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
static cpuid::cpuinfo& cpuInfo()
|
||||
{
|
||||
static cpuid::cpuinfo info;
|
||||
return info;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool SIMDDispatch<T>::getStatus(SIMDOps op) const
|
||||
{
|
||||
const unsigned index = static_cast<unsigned>(op);
|
||||
ASSERT(index < simdStatus.size());
|
||||
return simdStatus[index];
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void SIMDDispatch<T>::setStatus(SIMDOps op, bool enable)
|
||||
{
|
||||
const unsigned index = static_cast<unsigned>(op);
|
||||
ASSERT(index < simdStatus.size());
|
||||
|
||||
simdStatus[index] = enable;
|
||||
|
||||
const cpuid::cpuinfo& info = cpuInfo();
|
||||
bool useSSE = enable && info.has_sse();
|
||||
bool useAVX = enable && info.has_avx();
|
||||
|
||||
switch (op) {
|
||||
default:
|
||||
break;
|
||||
|
||||
case SIMDOps::writeInterleaved:
|
||||
if (useSSE)
|
||||
writeInterleaved = &writeInterleavedSSE;
|
||||
else
|
||||
writeInterleaved = &writeInterleavedScalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::readInterleaved:
|
||||
if (useSSE)
|
||||
readInterleaved = &readInterleavedSSE;
|
||||
else
|
||||
readInterleaved = &readInterleavedScalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::gain:
|
||||
if (useAVX) {
|
||||
applyGain = &applyGainAVX;
|
||||
applyGain1 = &applyGainAVX;
|
||||
}
|
||||
else if (useSSE) {
|
||||
applyGain = &applyGainSSE;
|
||||
applyGain1 = &applyGainSSE;
|
||||
}
|
||||
else {
|
||||
applyGain = &applyGainScalar<float>;
|
||||
applyGain1 = &applyGainScalar<float>;
|
||||
}
|
||||
break;
|
||||
|
||||
case SIMDOps::divide:
|
||||
if (useSSE)
|
||||
divide = ÷SSE;
|
||||
else
|
||||
divide = ÷Scalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::multiplyAdd:
|
||||
if (useSSE) {
|
||||
multiplyAdd = &multiplyAddSSE;
|
||||
multiplyAdd1 = &multiplyAddSSE;
|
||||
}
|
||||
else {
|
||||
multiplyAdd = &multiplyAddScalar<float>;
|
||||
multiplyAdd1 = &multiplyAddScalar<float>;
|
||||
}
|
||||
break;
|
||||
|
||||
case SIMDOps::linearRamp:
|
||||
if (useSSE)
|
||||
linearRamp = &linearRampSSE;
|
||||
else
|
||||
linearRamp = &linearRampScalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::multiplicativeRamp:
|
||||
if (useSSE)
|
||||
multiplicativeRamp = &multiplicativeRampSSE;
|
||||
else
|
||||
multiplicativeRamp = &multiplicativeRampScalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::add:
|
||||
if (useSSE) {
|
||||
add = &addSSE;
|
||||
add1 = &addSSE;
|
||||
}
|
||||
else {
|
||||
add = &addScalar<float>;
|
||||
add1 = &addScalar<float>;
|
||||
}
|
||||
break;
|
||||
|
||||
case SIMDOps::subtract:
|
||||
if (useSSE) {
|
||||
subtract = &subtractSSE;
|
||||
subtract1 = &subtractSSE;
|
||||
}
|
||||
else {
|
||||
subtract = &subtractScalar<float>;
|
||||
subtract1 = &subtractScalar<float>;
|
||||
}
|
||||
break;
|
||||
|
||||
case SIMDOps::copy:
|
||||
if (useSSE)
|
||||
copy = ©SSE;
|
||||
else
|
||||
copy = ©Scalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::cumsum:
|
||||
if (useSSE)
|
||||
cumsum = &cumsumSSE;
|
||||
else
|
||||
cumsum = &cumsumScalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::diff:
|
||||
if (useSSE)
|
||||
diff = &diffSSE;
|
||||
else
|
||||
diff = &diffScalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::mean:
|
||||
if (useSSE)
|
||||
mean = &meanSSE;
|
||||
else
|
||||
mean = &meanScalar<float>;
|
||||
break;
|
||||
|
||||
case SIMDOps::meanSquared:
|
||||
if (useSSE)
|
||||
meanSquared = &meanSquaredSSE;
|
||||
else
|
||||
meanSquared = &meanSquaredScalar<float>;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void SIMDDispatch<T>::resetStatus()
|
||||
{
|
||||
setStatus(SIMDOps::writeInterleaved, false);
|
||||
setStatus(SIMDOps::readInterleaved, false);
|
||||
setStatus(SIMDOps::fill, true);
|
||||
setStatus(SIMDOps::gain, true);
|
||||
setStatus(SIMDOps::divide, false);
|
||||
setStatus(SIMDOps::linearRamp, false);
|
||||
setStatus(SIMDOps::multiplicativeRamp, true);
|
||||
setStatus(SIMDOps::add, false);
|
||||
setStatus(SIMDOps::subtract, false);
|
||||
setStatus(SIMDOps::multiplyAdd, false);
|
||||
setStatus(SIMDOps::copy, false);
|
||||
setStatus(SIMDOps::cumsum, true);
|
||||
setStatus(SIMDOps::diff, false);
|
||||
setStatus(SIMDOps::sfzInterpolationCast, true);
|
||||
setStatus(SIMDOps::mean, false);
|
||||
setStatus(SIMDOps::meanSquared, false);
|
||||
setStatus(SIMDOps::upsampling, true);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
static volatile bool simdInitialized = false;
|
||||
static std::mutex simdMutex;
|
||||
|
||||
SIMDInitializer::SIMDInitializer()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock { simdMutex };
|
||||
|
||||
if (!simdInitialized) {
|
||||
simdDispatch.resetStatus();
|
||||
simdInitialized = true;
|
||||
}
|
||||
return diffScalar(input, output, size);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,9 +59,15 @@ enum class SIMDOps {
|
|||
};
|
||||
|
||||
// Enable or disable SIMD accelerators at runtime
|
||||
void resetSIMDOpStatus();
|
||||
void setSIMDOpStatus(SIMDOps op, bool status);
|
||||
bool getSIMDOpStatus(SIMDOps op);
|
||||
|
||||
// Initializer object which ensures to prepare SIMD dispatch
|
||||
struct SIMDInitializer {
|
||||
SIMDInitializer();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Read interleaved stereo data from a buffer and separate it in a left/right pair of buffers.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@
|
|||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
|
||||
template<class T>
|
||||
inline void readInterleavedScalar(const T* input, T* outputLeft, T* outputRight, unsigned inputSize)
|
||||
inline void readInterleavedScalar(const T* input, T* outputLeft, T* outputRight, unsigned inputSize) noexcept
|
||||
{
|
||||
const auto sentinel = input + inputSize - 1;
|
||||
while (input < sentinel) {
|
||||
|
|
@ -17,7 +18,7 @@ inline void readInterleavedScalar(const T* input, T* outputLeft, T* outputRight,
|
|||
}
|
||||
|
||||
template<class T>
|
||||
inline void writeInterleavedScalar(const T* inputLeft, const T* inputRight, T* output, unsigned outputSize)
|
||||
inline void writeInterleavedScalar(const T* inputLeft, const T* inputRight, T* output, unsigned outputSize) noexcept
|
||||
{
|
||||
const auto sentinel = output + outputSize - 1;
|
||||
while (output < sentinel) {
|
||||
|
|
@ -120,6 +121,12 @@ inline void subtractScalar(T value, T* output, unsigned size) noexcept
|
|||
*output++ -= value;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void copyScalar(const T* input, T* output, unsigned size) noexcept
|
||||
{
|
||||
std::copy(input, input + size, output);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
T meanScalar(const T* vector, unsigned size) noexcept
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,2 +1,12 @@
|
|||
#define CATCH_CONFIG_MAIN
|
||||
#include "sfizz/SIMDHelpers.h"
|
||||
|
||||
#define CATCH_CONFIG_RUNNER
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
sfz::SIMDInitializer simdInit;
|
||||
|
||||
int result = Catch::Session().run(argc, argv);
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue