Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to convert DXR HLSL to SPV_NV_ray_tracing #1920

Merged
merged 18 commits into from Feb 27, 2019

Conversation

Projects
None yet
6 participants
@alelenv
Copy link
Contributor

commented Feb 11, 2019

@ehsannas @antiagainst
Ehsan, Lei, please review the changes

Pattern match tests have been added for new features

Let us know if anything more is required from us
Thanks!

@msftclas

This comment has been minimized.

Copy link

commented Feb 11, 2019

CLA assistant check
All CLA requirements met.

@AppVeyorBot

This comment has been minimized.

@ehsannas ehsannas self-requested a review Feb 12, 2019

@ehsannas ehsannas added the spirv label Feb 12, 2019

@dneto0

This comment has been minimized.

Copy link

commented Feb 12, 2019

@alelenv : Thanks for this contribution!

For better inclusiveness, I'd prefer to see a copy of the HLSL functional spec in a more plain-text format, for better inclusiveness. Yes the .docx renders apparently correctly on my Mac, but it seems to be straining to do so.

I'm also a little concerned that the reference is on someone's personal .org page, and could disappear at a whim. Ideally a copy would be checked in with this PR (in the docs subtree). However, that doc lacks a copyright or author notice, so ... check with the author?

I don't mean to come off as overly nit-picky, but I'd like this to be a stable documented feature of the compiler.

@alelenv alelenv force-pushed the sparmarNV:NV_ray_tracing_final branch from 5c5b9a7 to 549193c Feb 12, 2019

@alelenv alelenv force-pushed the sparmarNV:NV_ray_tracing_final branch from b9bb79b to b5dcf5c Feb 12, 2019

@AppVeyorBot

This comment has been minimized.

@ehsannas
Copy link
Collaborator

left a comment

Hi @alelenv
Thanks for your contribution. It's great to see this work. The code looks good overall, but needs some improvements.

You'll find that many of my comments are related to adhering to the C++ style guide.

  • The style guide requires proper Punctuation, Spelling and Grammar. For example, comments should be proper sentences ending with a period. Consistently use SPIR-V, rather than spirv or spir-v or Spir-v, etc in the comments. I know this sounds nit-picky, but it'll lead to a much nicer code base over time (as it has happened over the development period of Spiregg).

  • clang-format should be applied to all the files that you have touched. (you can use this extension if you're using VisualStudio)

  • I'm very glad to see you have added new tests \o/ These will prevent regressions in RT code generation in the future. Please add more if you can.
    Here are a few that I thought might be useful:
    What happens if more than 1 function has the same [shader(X)] attribute? Add a test for it.
    Add a test that exercises the matrix transpose logic in processRayBuiltins.
    etc.
    Taking testing one step further: Would you be able to test the resulting SPIR-V on hardware to ensure correctness? I don't have access to the GPU that has the RT features. That would be the ultimate test for your work.

  • SPIR-V.rst needs to be updated (we'll discuss this more on the next round of code-review).

  • Please do not force push to your branch when addressing code-review comments. Please make new commits, so that we can review the diff more easily.

  • I'll do (at least one) more rounds of code-review and testing before landing.

Thanks!

@@ -268,6 +269,11 @@ bool isOrContainsNonFpColMajorMatrix(const ASTContext &,
/// matrix type.
QualType getComponentVectorType(const ASTContext &, QualType matrixType);

/// \brief Return a QualType corresponding to HLSL matrix of given element type

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

s/Return/Returns

@@ -420,6 +420,12 @@ class SpirvBuilder {

void createLineInfo(SpirvString *file, uint32_t line, uint32_t column);

/// \brief Create spirv instructions for NV raytracing ops

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

Use

/// \brief Creates SPIR-V instructions for NV raytracing ops.
@@ -1757,6 +1758,29 @@ class SpirvLineInfo : public SpirvInstruction {
uint32_t column;
};


/// \brief Base class for all NV raytracing instructions
/// These include following spirv opcodes

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator
/// These include the following SPIR-V opcodes:
@@ -1012,5 +1016,75 @@ QualType getComponentVectorType(const ASTContext &astContext,
return astContext.getExtVectorType(elemType, colCount);
}

QualType getHLSLMatrixType(ASTContext& astContext, Sema &S,

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

This is very useful. Thanks for creating the custom matrix QualType 👍

QualType getHLSLMatrixType(ASTContext& astContext, Sema &S,
ClassTemplateDecl *templateDecl,
QualType elemType, int rows, int columns)
{

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

the { should be on the previous line.
It looks like you haven't run clang-format ? If not, please do. Thanks.

ExecutionModel(ShaderKind sk, ExecModel em) : shaderKind(sk), execModel(em) {}

static const unsigned numExecutionModels = 14;
static const ExecutionModel executionModels[numExecutionModels];

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

In general static member variables are forbidden due to their lifetime and static initialization issues.

static functions are OK.

So, there are a couple of different ways you can address this:

  1. remove these static data members. Have the static getter functions construct an ExecutionModel object and return it. e.g.
static ExecutionModel GetByStageName(llvm::StringRef stageName);
static ExecutionModel GetByShaderKind(ShaderKind sk);

there'll be switch statements inside these functions I'd presume.

  1. Another way -- if we really really want to have only 14 objects and reuse them -- would be to use SpirvContext. The context class keeps all the objects that live for the lifetime of the backend. Given how lightweight the objects of this class are, I don't think it's necessary to go down this route.

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

Can this class be fully replaced with a standalone function (in SpirvEmitter.cpp) that returns the SPIR-V execution model for a given shader model kind?
e.g.

spv::ExecutionModel getSpirvExecutionModel(hlsl::ShaderModel::Kind smk);
bool IsRay() const {
return execModel >= ExecModel::RayGenerationNV &&
execModel <= ExecModel::CallableNV;
}

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

All these functions are reimplementing the logic in DxilShaderModel.h.
I wonder if we could just use the hlsl::ShaderModel instead of the ExecutionModel throughout the spiregg code, and remove ExecutionModel class completely? Is there a good reason for this class to exist?

This comment has been minimized.

Copy link
@sparmarNV

sparmarNV Feb 19, 2019

We can't use hlsl::ShaderModel because there are no ShaderModel objects for RT profiles in DxilShaderModel.cpp (rgen_6_4, miss_6_4, etc.).
So I implemented a similar class (ExecutionModel) which would allow me to track individual RT shader stages.
Nonetheless, I have removed ExecutionModel class and used ShaderModel::Kind to track current entry point.
Let me know if this change works for you.

@@ -380,6 +381,17 @@ class FunctionType : public SpirvType {
llvm::SmallVector<const SpirvType *, 8> paramTypes;
};

/// Represents accleration structure type as defined in SPV_NV_ray_tracing

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

SPV_NV_ray_tracing.

SpirvFunction *entryFunction;
bool isEntryFunction;

FunctionInfo()

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

you can remove this constructor.

if (const auto *shaderAttr = funcDecl->getAttr<HLSLShaderAttr>()) {
// If we are compiling as a library then add everything that has a
// ShaderAttr
FunctionInfo *entryInfo = new FunctionInfo(

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 13, 2019

Collaborator

As you may have noticed, we don't perform new memory allocation anywhere except in SpirvContext (which uses placement new) which is responsible for managing all allocated memory and releasing them.

I suggest you change your data structure from

llvm::DenseMap<const DeclaratorDecl *, const FunctionInfo *> functionInfoMap;

to

llvm::DenseMap<const DeclaratorDecl *, FunctionInfo> functionInfoMap;

and then

functionInfoMap[funcDecl] = FunctionInfo(...);
workQueue.insert(&functionInfoMap[funcDecl]);

... hmmm... it looks like you can further simplify this:

Remove functionInfoMap and change

llvm::SetVector<const DeclaratorDecl *> workQueue;

to

llvm::MapVector<const DeclaratorDecl *, FunctionInfo> workQueue;

this way, workQueue[funcDecl] is the same as what funcMapInfo[funcDecl] used to be ; and if I understand llvm::MapVector correctly, you can iterate over it the same as as SetVector, so you can still have the loop over workQueue.

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 25, 2019

Collaborator

@sparmarNV
I'm doing further testing on this PR, and I'm running into some failures. They are due to this suggestion that I made:

functionInfoMap[funcDecl] = FunctionInfo(...);
workQueue.insert(&functionInfoMap[funcDecl]);

It turns out llvm::DenseMap may choose to overwrite that address (&functionInfoMap[x]) when handling its buckets. Sorry about that. I have made the fix, and I'll post my fix to this PR directly. I have created a wrapper function that updates functionInfoMap and workQueue at the same time, and it uses placement new to allocate memory from the SpirvContext memory pool.

This comment has been minimized.

Copy link
@sparmarNV

sparmarNV Feb 25, 2019

Thanks for testing our changes and I am glad that you were able to triage and revert this change before the merge.

@alelenv

This comment has been minimized.

Copy link
Contributor Author

commented Feb 13, 2019

@dneto0 : Sure. We are working on providing you a text copy soon which we can check into docs.

@ehsannas : Thanks for such a detailed review.
Some quick answers below. sparmarNV will be handling issues related to multiple entry points support.
We will update branch based on your feedback soon.

Q) Add a test that exercises the matrix transpose logic in processRayBuiltins. etc.
ObjectToWorld3x4() & WorldToObject3x4() are transposes of SPIR-V Builtin
A) This is already tested in raytracing.nv.closesthit.hlsl/raytracing.nv.anyhit.hlsl

Q) Taking testing one step further: Would you be able to test the resulting SPIR-V on hardware to ensure correctness? I don't have access to the GPU that has the RT features. That would be the ultimate test for your work.
A) We have some internal apps running on HW using the generated SPIR-V.

@AppVeyorBot

This comment has been minimized.

@ehsannas

This comment has been minimized.

Copy link
Collaborator

commented Feb 14, 2019

A) This is already tested in raytracing.nv.closesthit.hlsl/raytracing.nv.anyhit.hlsl

Cool! I couldn't find OpTranspose in your CHECK: commands in the tests. So it would be good to add a CHECK. This will firstly ensure OpTranspose is actually called (and not accidentally removed in the future), and it'll also be good to have a check to see the custom matrix type (e.g. 4x3 or 3x4) as the result type in a CHECK.

A) We have some internal apps running on HW using the generated SPIR-V.

Awesome! Glad to hear that.

Update tests to verify transpose and custom matrix type
Update tests to add multple entry functions of same shader stage
@alelenv

This comment has been minimized.

Copy link
Contributor Author

commented Feb 19, 2019

HLSL Spec is provided by Microsoft to users registered to DX forums
Please follow instructions provided below to register
http://forums.directxtech.com/index.php?topic=5985.0

I have updated link above to publicly available HLSL specs from MSDN

@AppVeyorBot

This comment has been minimized.

Replace ExecutionModel class with ShaderModel::Kind
- This change removes ExecutionModel class and relies on ShaderModel::Kind to track current entry point shader stage
- Also instead of declaring it in SpirvEmitter, DeclResultIdMapper & GlPerVertex, we declare it only once in common object SpirvContext
@AppVeyorBot

This comment has been minimized.

@ehsannas
Copy link
Collaborator

left a comment

@sparmarNV

Thanks for making this change 👍 . This is much nicer than what was there before.
You have eliminated a whole class, eliminated the use of static members, eliminated copies of ExecutionModel in different places and concentrated it all in SpirvContext. All great changes 👍 .

Please run clang-format if you haven't already.

declIdMapper.setSpvExecutionModel(curEntryOrCallee->spvExecModel);
spvExecModel = curEntryOrCallee->spvExecModel;
entryFunction = curEntryOrCallee->entryFunction;
spvContext.setCurrentShaderModelKind(curEntryOrCallee->shaderModelKind);

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

Great to see the 3 lines is replaced with 1 line. Shader kind is now only stored in 1 place (SpirvContext), and this makes the code less error-prone. 👍

// Current ShaderModelKind for entry point.
ShaderModelKind curShaderModelKind;
// Major/Minor hlsl profile version.
unsigned majorVersion;

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

use uint32_t instead of unsigned to be consistent with the rest of the code.

This comment has been minimized.

Copy link
@alelenv

alelenv Feb 25, 2019

Author Contributor

@sparmarNV : Could you comment on this?

This comment has been minimized.

Copy link
@sparmarNV

sparmarNV Feb 25, 2019

I see both unsigned and uint32_t being used in the file.
Nonetheless, I will update the type to uint32_t.

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 25, 2019

Collaborator

Looks like a few unsigned have snuck through in the past. But let's strive to stay consistent. I'll update the other ones to also use uint32_t in a different PR. Thanks for updating.

@@ -366,12 +362,6 @@ class DeclResultIdMapper {
/// \brief Sets the entry function.
void setEntryFunction(SpirvFunction *fn) { entryFunction = fn; }

/// \brief Sets the SPIR-V execution model

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

Happy to see this is gone.

@@ -648,15 +638,13 @@ class DeclResultIdMapper {
inline bool isInputStorageClass(const StageVar &v);

private:
const hlsl::ShaderModel &shaderModel;

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

Happy to see this is gone.

SpirvBuilder &spvBuilder;
SpirvEmitter &theEmitter;
const SpirvCodeGenOptions &spirvOptions;
ASTContext &astContext;
SpirvContext &spvContext;
DiagnosticsEngine &diags;
SpirvFunction *entryFunction;
const ExecutionModel *spvExecModel;

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

Happy to see this is gone.

ExecutionModel::GetByShaderKind(shaderModel.GetKind()), funcDecl,
/*entryFunction*/ nullptr, /*isEntryFunc*/ true);
FunctionInfo *entryInfo =
new FunctionInfo(spvContext.getCurrentShaderModelKind(), funcDecl,

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

I believe new should be avoided here. See my comment about workQueue

This comment has been minimized.

Copy link
@sparmarNV

sparmarNV Feb 21, 2019

Yes, I am working on this next. Thanks for the review 👍

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 21, 2019

Collaborator

Cool. Thanks 👍

This comment has been minimized.

Copy link
@sparmarNV

sparmarNV Feb 21, 2019

I tried replacing functionInfoMap and workQueue with a MapVector, but it seems MapVector has few limitations.
We can't access the vector elements inside the MapVector with an index, which doesn't work in below case -

// The queue can grow in the meanwhile; so need to keep evaluating
// workQueue.size().
for (uint32_t i = 0; i < workQueue.size(); ++i) {
const FunctionInfo *curEntryOrCallee = workQueue[i];
...
}

Also using iterator on MapVector doesn't work with growing queue.
So I have gone with your option 1, which is to remove 'new' memory allocation by changing the DenseMap decl to -

llvm::DenseMap<const DeclaratorDecl *, FunctionInfo> functionInfoMap;

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 22, 2019

Collaborator

@sparmarNV Ah you're right. It is not possible to iterate over a MapVector when it is growing. Thanks for removing the usage of new.

@@ -613,14 +618,19 @@ void SpirvEmitter::HandleTranslationUnit(ASTContext &context) {
spvBuilder.setMemoryModel(spv::AddressingModel::Logical,
spv::MemoryModel::GLSL450);

// Even though the 'workQueue' grows due to the above loop, the first
// 'numEntryPoints' entries in the 'workQueue' are the ones with the HLSL
// 'shader' attribute, and must therefore be entry functions.

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

Thanks for adding.

FunctionInfo *calleeInfo = new FunctionInfo(
spvExecModel, callee, /*entryFunction*/ nullptr, /*isEntryFunc*/ false);
FunctionInfo *calleeInfo =
new FunctionInfo(spvContext.getCurrentShaderModelKind(), callee,

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

I believe new should be avoided here. See my comment about workQueue

@@ -593,6 +592,11 @@ class SpirvEmitter : public ASTConsumer {
/// Emits an error if the given attribute is not a loop attribute.
spv::LoopControlMask translateLoopAttribute(const Stmt *, const Attr &);

static hlsl::ShaderModel::Kind
SpirvEmitter::getShaderModelKind(StringRef stageName);

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

No need for SpirvEmitter::

static hlsl::ShaderModel::Kind getShaderModelKind(StringRef stageName);

This is caught by buildbots on Linux and macOS
Same for getSpirvShaderStage below.

@@ -1,62 +0,0 @@
//===------- ExecutionModel.h - get/set SPV Execution Model -----*- C++ -*-===//

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 20, 2019

Collaborator

Happy to see that the class is gone. Especially the static data that was included in it.

@AppVeyorBot

This comment has been minimized.

Don't perform 'new' memory allocation for FunctionInfo object
This change also -
- removes invalid "SpirvEmitter::" from function declarations in SpirvEmitter class.
- fix build errors by adding a default constructor in FunctionInfo struct to allow functionInfoMap allocate an empty object for no search results.
@AppVeyorBot

This comment has been minimized.

@alelenv

This comment has been minimized.

Copy link
Contributor Author

commented Feb 22, 2019

@ehsannas : I believe we have completed updates based on your feedback for first round of reviews
Can you point us to next steps i.e how to update SPIR-V.rst?
Thanks

@ehsannas
Copy link
Collaborator

left a comment

@alelenv Thanks for addressing my comments so far. If you go through all the files, there are a few (mostly formatting) ones left to fix.

We're almost there.

Regarding documentation:
We have a comprehensive doc file about mapping HLSL concepts to SPIR-V. The file (SPIR-V.rst) is located here. You should update this file to reflect the changes you have made in this PR.

I think it'd be good to add a top-level section about Ray Tracing between Shader Model 6.0 Wave Intrinsics and Supported Command-line Options sections. And in that Ray Tracing section, feel free to add as many sub-sections as you need. The goal is to highlight the the RTX concepts/features and how they map from HLSL to SPIR-V. In other sections of this document we've tried to provide simple explanations for people to get started, and provide external links for full details. For example, you can also provide a link to the RTX HLSL spec, and the SPIR-V NV RT extension spec. It'd also be really nice to provide a link to sample shaders (e.g. raytracing helloworld shaders) that people can use to get started.

Under HLSL Shader Stages, you should add a Ray Tracing Stages subsection, and add the newly supported shader stages under that. For each stage, it'd be good to provide some explanation of the stage and also list the expected shader inputs and outputs (stage variables). Including Figure 1 and Figure 5 from @nsubtil's blog post could be very useful here.

In general I found that blog post to be quite a good introduction. You could use some of its content as brief explanations for the new documentation you are adding to SPIR-V.rst.

Thanks!

@@ -34,7 +34,8 @@ bool SpirvFunction::invokeVisitor(Visitor *visitor, bool reverseOrder) {
if (!basicBlocks.empty()) {
BlockReadableOrderVisitor([&orderedBlocks](SpirvBasicBlock *block) {
orderedBlocks.push_back(block);
}).visit(basicBlocks.front());
})

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 22, 2019

Collaborator

revert to previous format (clang-format).

@@ -1046,6 +1084,16 @@ class SpirvEmitter : public ASTConsumer {
/// Maps a given statement to the basic block that is associated with it.
llvm::DenseMap<const Stmt *, SpirvBasicBlock *> stmtBasicBlock;

/// Maintains mapping from a type to spirv variable along with spirv

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 22, 2019

Collaborator

s/spirv/SPIR-V

@@ -420,6 +420,12 @@ class SpirvBuilder {

void createLineInfo(SpirvString *file, uint32_t line, uint32_t column);

/// \brief Creates spirv instructions for NV raytracing ops

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 22, 2019

Collaborator

s/spirv/SPIR-V.
ops.

SpirvInstruction *retVal =
declIdMapper.getBuiltinVar(builtin, builtinType, callExpr->getExprLoc());
retVal = spvBuilder.createLoad(builtinType, retVal);
if (transposeMatrix)

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 22, 2019

Collaborator

This is done now. Thanks.

spvBuilder.createLoad(hitAttributeArg->getType(), hitAttributeArgInst);
spvBuilder.createStore(hitAttributeStageVar, tempLoad);

// SPV Instruction :

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 22, 2019

Collaborator

s/SPV/SPIR-V

builtin = spv::BuiltIn::WorldToObjectNV;
break;
default:
emitError("ray intrinsic function unimplemented", callExpr->getExprLoc());

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 22, 2019

Collaborator

Should we return nullptr after emitError?

spvBuilder.createLoad(callDataArg->getType(), callDataArgInst);
spvBuilder.createStore(callDataStageVar, tempLoad);

// SPV Instruction

This comment has been minimized.

Copy link
@ehsannas

ehsannas Feb 22, 2019

Collaborator

s/SPV/SPIR-V

@AppVeyorBot

This comment has been minimized.

@AppVeyorBot

This comment has been minimized.

@AppVeyorBot

This comment has been minimized.

@ehsannas

This comment has been minimized.

Copy link
Collaborator

commented Feb 25, 2019

@alelenv @sparmarNV Thanks for your updates.
Sorry I forgot to mention one thing about the doc file:
There's a Intrinsic functions section in the doc file. Please add a subsection for Ray Tracing Intrinsics, and put in a table (example) to show the HLSL-->SPIR-V mapping for the new intrinsics that you have added support for.
Thanks!

@AppVeyorBot

This comment has been minimized.

ehsannas and others added some commits Feb 25, 2019

Use placement new to allocate FunctionInfo objects.
Also bundle the insertion into functionInfoMap and workQueue together.
@alelenv

This comment has been minimized.

Copy link
Contributor Author

commented Feb 25, 2019

@ehsannas : Sorry for missing a large number of formatting fixes from previous comments. My tracking changes for each comment via emails didnt work out properly.
I have updated rst with raytracing info, mappings and flowchart diagrams
Let me know your thoughts

Thanks!

@AppVeyorBot

This comment has been minimized.

@ehsannas

This comment has been minimized.

Copy link
Collaborator

commented Feb 26, 2019

Just finished reading the documentation. Awesome job, @alelenv 👍. This is exactly what we were looking for.
It provides a good overview and contains links for readers to go off and learn more (specs and tutorials).

@ehsannas
Copy link
Collaborator

left a comment

LGTM. Thanks for addressing all the code review comments \o/
I will do a bit of more testing, and if all goes well, I'll merge the PR (hopefully by tomorrow).

@AppVeyorBot

This comment has been minimized.

@AppVeyorBot

This comment has been minimized.

@ehsannas ehsannas merged commit e58ecd4 into microsoft:master Feb 27, 2019

3 checks passed

continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
license/cla All CLA requirements met.
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.