DX12 之 miniEngine 代码自解

本贴最后更新于 2162 天前,其中的信息可能已经时移俗易

好像有个叫 Vulkan 的图形 API 试图替代 Opengl,不过要跟上时代,还是得把 WIN10 装上,试试看在 DX12 写个下个时代的 3D 设计工具,当然只是个小玩意,不过据说很多软件能走到今天,也是因为积淀,如果不早点开始积淀,就永远会落后那么多
我想给这个玩意起个名字,叫啥呢,就叫 W4D 好了,首先这个软件定位不是游戏引擎,而且是超轻量的,W:windows 4:x y z t D: Data 指代在 windows 下处理 3 维与时间数据的软件
想要有什么呢?
1:首先需要能应对基本的 3D 建模,绑骨,展 UV 贴图,动作
2:要有语言能应对复杂的各种情况 类似 maxscript 脚本语言,但是这里可能级别要更强
3:最好有时间轴,觉得这个概念与 2D 里 flash 接近,跟 unity 里 animation 差不多,用不着搞成 timeline 那么复杂。
代码使用 C++ DX,确定要一切考虑效率,即使函数入口都不要检查,只在最外层进行检查,出错是程序员的事,代码不是用来为程序员负责的,希望这个玩意能像刀片一样 欻欻的就搞定哈哈

环境 WIN10,VS2017.这二者搞定后,微软官方例子可以直接打开编译运行的,miniEngine 可能要重定向项目-> 选择相应的 SDK 版本(在解决方案右键会出来选项)

dx12 基础
http://www.rastertek.com/tutdx12.html
官网的
https://msdn.microsoft.com/en-us/library/windows/desktop/dn899121(v=vs.85).aspx

Working samples

例子
Working samples (in the form of Visual Studio 2015 projects) can be downloaded from GitHub/Microsoft/DirectX-Graphics-Samples.

DX12 遍历显卡
Intel(R) HD Graphics 4600
NVIDIA GeForce GTX 960M
Microsoft Basic Render Driver

把特性设置为 D3D_FEATURE_LEVEL_11_0

HRESULT hr = CreateDXGIFactory(__uuidof(IDXGIFactory4), (void**)&factory);
int i = 0;
IDXGIAdapter * pAdapter;
std::vector <IDXGIAdapter*> vAdapters;
while (factory->EnumAdapters(i, &pAdapter) != DXGI_ERROR_NOT_FOUND)
{
//hr = D3D12CreateDevice(pAdapter, feature_level, __uuidof(ID3D12Device), (void**)& pDevice);
//if (SUCCEEDED(hr))
// break;
pAdapter->GetDesc(&adapterDesc);
wcstombs_s(&stringLength, (char*)videoCardDes, 128, adapterDesc.Description, 128);
vAdapters.push_back(pAdapter);
++i;
}

http://www.rastertek.com/
这个网站上 3 篇 DX12 的已经照着写了一遍,感觉概念多,复杂,勉强能跑个背景色出来就废了这么大功夫,可惜的是作者似乎没有继续写了
还是看 dx12 在 git 上的例子好了。

哎呀,感觉好难啊。
打开微软 ModelViewer_VS15.sln 的例子,运行效果还是挺好的,就是连入口都摸了半天
场景示例

入口是一个宏
CREATE_APPLICATION( ModelViewer )

映射到 GameCore::RunApplication( app_class(), L#app_class );
namespace GameCore 里定义了这个全局函数 RunApplication
这个函数初始化了引擎的各个部分,注册了窗口类,创建了消息循环,循环结束后,进行各个部分的资源清理,基本所有引擎框架都有类似这样的一个结构吧。
不过这个函数的入口是 c 语言的 int wmain(int argc, wchar_t** argv),估计是要通过宏适应多平台吧。

在 GameCore::RunApplication 里看下有哪些模块都在这里初始化的
在创建完窗口后调用了
InitializeApplication(app);
定义为
void InitializeApplication( IGameApp& game )
{
Graphics::Initialize();
SystemTime::Initialize();
GameInput::Initialize();
EngineTuning::Initialize();

game.Startup();

}
这个 game 参数,就是 ModelViewer 的一个对象

这部分基本和
www.rastertek.com
描述的差不多,图形初始化,输入初始化,游戏初始化,系统时间初始化,引擎协调初始化
Graphics::Initialize();在这里,
创建了 dx12 设备
(遍历了一次取了显存最大的显卡,还过滤掉了软件模拟卡)

SIZE_T MaxSize = 0;

for (uint32_t Idx = 0; DXGI_ERROR_NOT_FOUND != dxgiFactory->EnumAdapters1(Idx, &pAdapter); ++Idx)
{
DXGI_ADAPTER_DESC1 desc;
pAdapter->GetDesc1(&desc);
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
continue;

if (desc.DedicatedVideoMemory > MaxSize && SUCCEEDED(D3D12CreateDevice(pAdapter.Get(), D3D_FEATURE_LEVEL_11_0, MY_IID_PPV_ARGS(&pDevice))))
{
	pAdapter->GetDesc1(&desc);
	Utility::Printf(L"D3D12-capable hardware found:  %s (%u MB)\n", desc.Description, desc.DedicatedVideoMemory >> 20);
	MaxSize = desc.DedicatedVideoMemory;
}

}

if (MaxSize > 0)
g_Device = pDevice.Detach();

然后检查了设备是否支持 R11G11B10 和 R16G16B16A16 2 种 UAVLoadSupport 上传格式,这部分仅仅设置了几个变量标识是否有此特性(g_bTypedUAVLoadSupport_R11G11B10_FLOAT,g_bTypedUAVLoadSupport_R16G16B16A16_FLOAT)。

D3D12_FEATURE_DATA_D3D12_OPTIONS FeatureData = {};
if (SUCCEEDED(g_Device->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS, &FeatureData, sizeof(FeatureData))))
{
if (FeatureData.TypedUAVLoadAdditionalFormats)
{
D3D12_FEATURE_DATA_FORMAT_SUPPORT Support =
{
DXGI_FORMAT_R11G11B10_FLOAT, D3D12_FORMAT_SUPPORT1_NONE, D3D12_FORMAT_SUPPORT2_NONE
};

	if (SUCCEEDED(g_Device->CheckFeatureSupport(D3D12_FEATURE_FORMAT_SUPPORT, &Support, sizeof(Support))) &&
		(Support.Support2 & D3D12_FORMAT_SUPPORT2_UAV_TYPED_LOAD) != 0)
	{
		g_bTypedUAVLoadSupport_R11G11B10_FLOAT = true;
	}

	Support.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;

	if (SUCCEEDED(g_Device->CheckFeatureSupport(D3D12_FEATURE_FORMAT_SUPPORT, &Support, sizeof(Support))) &&
		(Support.Support2 & D3D12_FORMAT_SUPPORT2_UAV_TYPED_LOAD) != 0)
	{
		g_bTypedUAVLoadSupport_R16G16B16A16_FLOAT = true;
	}
}

}

接着创建了各种 commandqueue,
g_CommandManager.Create(g_Device);
g_CommandManager 是 CommandListManager 类型的
跟进去看一下
void CommandListManager::Create(ID3D12Device* pDevice)
{
ASSERT(pDevice != nullptr);

m_Device = pDevice;

m_GraphicsQueue.Create(pDevice);
m_ComputeQueue.Create(pDevice);
m_CopyQueue.Create(pDevice);

}

m_GraphicsQueue m_ComputeQueue m_CopyQueue 都是 CommandQueue,这里的实现都一样

void CommandQueue::Create(ID3D12Device* pDevice)
{
ASSERT(pDevice != nullptr);
ASSERT(!IsReady());
ASSERT(m_AllocatorPool.Size() == 0);

D3D12_COMMAND_QUEUE_DESC QueueDesc = {};
QueueDesc.Type = m_Type;
QueueDesc.NodeMask = 1;
pDevice->CreateCommandQueue(&QueueDesc, MY_IID_PPV_ARGS(&m_CommandQueue));
m_CommandQueue->SetName(L"CommandListManager::m_CommandQueue");

ASSERT_SUCCEEDED(pDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, MY_IID_PPV_ARGS(&m_pFence)));
m_pFence->SetName(L"CommandListManager::m_pFence");
m_pFence->Signal((uint64_t)m_Type << 56);

m_FenceEventHandle = CreateEvent(nullptr, false, false, nullptr);
ASSERT(m_FenceEventHandle != INVALID_HANDLE_VALUE);

m_AllocatorPool.Create(pDevice);

ASSERT(IsReady());

}

这个类应该算对 dx12 commandqueue 的封装吧,核心就是使用 pDevice->CreateCommandQueue(&QueueDesc, MY_IID_PPV_ARGS(&m_CommandQueue));
然后创建了 fence,fence_event
m_AllocatorPool.Create(pDevice);只是把类的成员变量设置为 pDevice,这个 m_AllocatorPool 也是对 commandAllocator 的一个池的封装 他成员里有 std::vector<ID3D12CommandAllocator*> m_AllocatorPool;
这部分完了,返回堆栈上面的 Graphics::Initialize();

在 g_CommandManager.Create(g_Device);之后

DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.Width = g_DisplayWidth;
swapChainDesc.Height = g_DisplayHeight;
swapChainDesc.Format = SwapChainFormat;
swapChainDesc.Scaling = DXGI_SCALING_NONE;
swapChainDesc.SampleDesc.Quality = 0;
swapChainDesc.SampleDesc.Count = 1;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.BufferCount = SWAP_CHAIN_BUFFER_COUNT;
swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;

#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) // Win32
ASSERT_SUCCEEDED(dxgiFactory->CreateSwapChainForHwnd(g_CommandManager.GetCommandQueue(), GameCore::g_hWnd, &swapChainDesc, nullptr, nullptr, &s_SwapChain1));

创建了一个交换链的描述,然后通过这个描述(swapChainDesc)创建了一个交换链,g_CommandManager.GetCommandQueue()返回的是他成员里,m_GraphicsQueue 创建的 CommandQueue, 交换链接口的指针放到 s_SwapChain1 里
下面有部分代码不明白意思,跳过去

#if CONDITIONALLY_ENABLE_HDR_OUTPUT && defined(NTDDI_WIN10_RS2) && (NTDDI_VERSION >= NTDDI_WIN10_RS2)
{
IDXGISwapChain4* swapChain = (IDXGISwapChain4*)s_SwapChain1;
ComPtr output;
ComPtr output6;
DXGI_OUTPUT_DESC1 outputDesc;
UINT colorSpaceSupport;

// Query support for ST.2084 on the display and set the color space accordingly
if (SUCCEEDED(swapChain->GetContainingOutput(&output)) &&
	SUCCEEDED(output.As(&output6)) &&
	SUCCEEDED(output6->GetDesc1(&outputDesc)) &&
	outputDesc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 &&
	SUCCEEDED(swapChain->CheckColorSpaceSupport(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020, &colorSpaceSupport)) &&
	(colorSpaceSupport & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) &&
	SUCCEEDED(swapChain->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020)))
{
	g_bEnableHDROutput = true;
}

}
#endif
看英文解释是是否支持 ST.2048 并设置交换链的颜色空间的。

接着看
for (uint32_t i = 0; i < SWAP_CHAIN_BUFFER_COUNT; ++i)
{
ComPtr DisplayPlane;
ASSERT_SUCCEEDED(s_SwapChain1->GetBuffer(i, MY_IID_PPV_ARGS(&DisplayPlane)));
g_DisplayPlane[i].CreateFromSwapChain(L"Primary SwapChain Buffer", DisplayPlane.Detach());
}

这部分是创建交换链使用的资源以及相应的 RenderTargetView,一个交换链上创建数个 RTV,话说这个包装还是有点难理解的。
交换链上 GetBuffer 类似按照指定的序号(在这里就是 i),把 ID3D12Resource 指针返回,要使用就创建一个 ID3D12Resource 描述的 RTV 对象
这个 RTV 对象是通过
g_DisplayPlane[i].CreateFromSwapChain(L"Primary SwapChain Buffer", DisplayPlane.Detach())创建的
g_DisplayPlane 是一个 ColorBuffer 数组
跟进去看一下

void ColorBuffer::CreateFromSwapChain( const std::wstring& Name, ID3D12Resource* BaseResource )
{
AssociateWithResource(Graphics::g_Device, Name, BaseResource, D3D12_RESOURCE_STATE_PRESENT);

//m_UAVHandle[0] = Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
//Graphics::g_Device->CreateUnorderedAccessView(m_pResource.Get(), nullptr, nullptr, m_UAVHandle[0]);

m_RTVHandle = Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
Graphics::g_Device->CreateRenderTargetView(m_pResource.Get(), nullptr, m_RTVHandle);

}

再跟进
AssociateWithResource(Graphics::g_Device, Name, BaseResource, D3D12_RESOURCE_STATE_PRESENT);

ColorBuffer 继承 PixelBuffer,跳转到这里

void PixelBuffer::AssociateWithResource( ID3D12Device* Device, const std::wstring& Name, ID3D12Resource* Resource, D3D12_RESOURCE_STATES CurrentState )
{
(Device); // Unused until we support multiple adapters

ASSERT(Resource != nullptr);
D3D12_RESOURCE_DESC ResourceDesc = Resource->GetDesc();

m_pResource.Attach(Resource);
m_UsageState = CurrentState;

m_Width = (uint32_t)ResourceDesc.Width;		// We don't care about large virtual textures yet
m_Height = ResourceDesc.Height;
m_ArraySize = ResourceDesc.DepthOrArraySize;
m_Format = ResourceDesc.Format;

#ifndef RELEASE
m_pResource->SetName(Name.c_str());
#else
(Name);
#endif
}

这个函数只是调用 ID312DResources 指针获取了资源描述 GetDesc(),然后把这些描述的属性记在了自己的成员里
跳出 AssociateWithResource 函数返回
void ColorBuffer::CreateFromSwapChain( const std::wstring& Name, ID3D12Resource* BaseResource )
{
AssociateWithResource(Graphics::g_Device, Name, BaseResource, D3D12_RESOURCE_STATE_PRESENT);//跳出堆栈

//m_UAVHandle[0] = Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
//Graphics::g_Device->CreateUnorderedAccessView(m_pResource.Get(), nullptr, nullptr, m_UAVHandle[0]);
 //到这里
m_RTVHandle = Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
Graphics::g_Device->CreateRenderTargetView(m_pResource.Get(), nullptr, m_RTVHandle);

}

m_RTVHandle 大概是一个 RTV 对象描述 的句柄。
跟进 Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);内部
inline D3D12_CPU_DESCRIPTOR_HANDLE AllocateDescriptor( D3D12_DESCRIPTOR_HEAP_TYPE Type, UINT Count = 1 )
{
return g_DescriptorAllocator[Type].Allocate(Count);
}
实际上最后是用 g_DescriptorAllocator[Type].Allocate(Count)返回了一个句柄,看下 g_DescriptorAllocator 定义
DescriptorAllocator g_DescriptorAllocator[D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES] =
{
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,
D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER,
D3D12_DESCRIPTOR_HEAP_TYPE_RTV,
D3D12_DESCRIPTOR_HEAP_TYPE_DSV,
};
看的到这是一个 DescriptorAllocator 类型的数组,这个 g_DescriptorAllocator 数组有 4 个成员,每个成员都是一个
DescriptorAllocator 类型对象,这个类型对堆申请以及句柄申请做了一个池的封装

跟到 Allocate 里看一下
D3D12_CPU_DESCRIPTOR_HANDLE DescriptorAllocator::Allocate( uint32_t Count )
{
if (m_CurrentHeap == nullptr || m_RemainingFreeHandles < Count)
{
m_CurrentHeap = RequestNewHeap(m_Type);
m_CurrentHandle = m_CurrentHeap->GetCPUDescriptorHandleForHeapStart();
m_RemainingFreeHandles = sm_NumDescriptorsPerHeap;

    if (m_DescriptorSize == 0)
        m_DescriptorSize = Graphics::g_Device->GetDescriptorHandleIncrementSize(m_Type);
}

D3D12_CPU_DESCRIPTOR_HANDLE ret = m_CurrentHandle;
m_CurrentHandle.ptr += Count * m_DescriptorSize;
m_RemainingFreeHandles -= Count;
return ret;

}

没有堆就创建一个堆
跟到 m_CurrentHeap = RequestNewHeap(m_Type);这里
参数 m_Type 就跟 g_DescriptorAllocator[Type]的 Type 一样就是 D3D12_DESCRIPTOR_HEAP_TYPE_RTV

ID3D12DescriptorHeap* DescriptorAllocator::RequestNewHeap(D3D12_DESCRIPTOR_HEAP_TYPE Type)
{
std::lock_guard LockGuard(sm_AllocationMutex);

D3D12_DESCRIPTOR_HEAP_DESC Desc;
Desc.Type = Type;
Desc.NumDescriptors = sm_NumDescriptorsPerHeap;//256个描述
Desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
Desc.NodeMask = 1;

Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> pHeap;
ASSERT_SUCCEEDED(Graphics::g_Device->CreateDescriptorHeap(&Desc, MY_IID_PPV_ARGS(&pHeap)));
sm_DescriptorHeapPool.emplace_back(pHeap);
return pHeap.Get();

}

函数实现还带锁,那应该是提供给多线程使用的,这里也就是创建了一个堆描述对象,这个堆描述对象的 NumDescriptors 为 256(sm_NumDescriptorsPerHeap = 256)
最后还是通过
g_Device->CreateDescriptorHeap(&Desc, MY_IID_PPV_ARGS(&pHeap);
创建了一个描述堆。
返回了创建的 ID3D12DescriptorHeap 指针
m_CurrentHandle = m_CurrentHeap->GetCPUDescriptorHandleForHeapStart();
这个意思就是取得描述堆起始的句柄
if (m_DescriptorSize == 0)
m_DescriptorSize = Graphics::g_Device->GetDescriptorHandleIncrementSize(m_Type);
这个就是取得每 2 个相邻句柄的指针要偏移多少

D3D12_CPU_DESCRIPTOR_HANDLE ret = m_CurrentHandle;
m_CurrentHandle.ptr += Count * m_DescriptorSize;
m_RemainingFreeHandles -= Count;
return ret;

这个就是说 Count 参数,申请多少个句柄,用 ret 存储返回的句柄,把原句柄按照数量偏移到新位置去。返回 ret(保存着偏移前的位置的句柄)

返回到
m_RTVHandle = Graphics::AllocateDescriptor(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
接着
Graphics::g_Device->CreateRenderTargetView(m_pResource.Get(), nullptr, m_RTVHandle);
这句话意思类似:创建一个如 m_pResource(ID3D12Resource 指针)描述的资源对象,并把对象句柄存储在 m_RTVHandle 里,从此 m_RTVHandle 就真正指向了一个 RTV 对象
返回到堆栈上

for (uint32_t i = 0; i < SWAP_CHAIN_BUFFER_COUNT; ++i)
{
ComPtr DisplayPlane;
ASSERT_SUCCEEDED(s_SwapChain1->GetBuffer(i, MY_IID_PPV_ARGS(&DisplayPlane)));
g_DisplayPlane[i].CreateFromSwapChain(L"Primary SwapChain Buffer", DisplayPlane.Detach());
}
循环里每一次运行,就申请了一个 RTVHandler,然后创建了一个 RTV 对象放到这个 RTVHandler 中,这个循环一共运行了 3 次(SWAP_CHAIN_BUFFER_COUNT = 3)

往下
太多代码还未理解。
觉得 dx12 还是需要先把概念扔出来,而不是这里这样那里那样,有了整体的概念,应该好理解很多

这里有一部分描述
https://www.csdn.net/article/a/2016-01-04/15833516

代码里有些类似乎涉及到一些算法,搜索了些资料保存下来慢慢看
双调排序 Bitonic Sort

一些名词
环境光遮蔽

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    288 引用 • 4485 回帖 • 663 关注
  • AngularJS

    AngularJS 诞生于 2009 年,由 Misko Hevery 等人创建,后为 Google 所收购。是一款优秀的前端 JS 框架,已经被用于 Google 的多款产品当中。AngularJS 有着诸多特性,最为核心的是:MVC、模块化、自动化双向数据绑定、语义化标签、依赖注入等。2.0 版本后已经改名为 Angular。

    12 引用 • 50 回帖 • 483 关注
  • Ant-Design

    Ant Design 是服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计者和开发者专注于更好的用户体验。

    17 引用 • 23 回帖 • 4 关注
  • 禅道

    禅道是一款国产的开源项目管理软件,她的核心管理思想基于敏捷方法 scrum,内置了产品管理和项目管理,同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能,在一个软件中就可以将软件研发中的需求、任务、bug、用例、计划、发布等要素有序的跟踪管理起来,完整地覆盖了项目管理的核心流程。

    5 引用 • 15 回帖 • 101 关注
  • Oracle

    Oracle(甲骨文)公司,全称甲骨文股份有限公司(甲骨文软件系统有限公司),是全球最大的企业级软件公司,总部位于美国加利福尼亚州的红木滩。1989 年正式进入中国市场。2013 年,甲骨文已超越 IBM,成为继 Microsoft 后全球第二大软件公司。

    105 引用 • 127 回帖 • 370 关注
  • 区块链

    区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。所谓共识机制是区块链系统中实现不同节点之间建立信任、获取权益的数学算法 。

    91 引用 • 751 回帖 • 1 关注
  • 服务器

    服务器,也称伺服器,是提供计算服务的设备。由于服务器需要响应服务请求,并进行处理,因此一般来说服务器应具备承担服务并且保障服务的能力。

    125 引用 • 588 回帖
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 394 关注
  • VirtualBox

    VirtualBox 是一款开源虚拟机软件,最早由德国 Innotek 公司开发,由 Sun Microsystems 公司出品的软件,使用 Qt 编写,在 Sun 被 Oracle 收购后正式更名成 Oracle VM VirtualBox。

    10 引用 • 2 回帖
  • Bug

    Bug 本意是指臭虫、缺陷、损坏、犯贫、窃听器、小虫等。现在人们把在程序中一些缺陷或问题统称为 bug(漏洞)。

    76 引用 • 1737 回帖 • 1 关注
  • SOHO

    为成为自由职业者在家办公而努力吧!

    7 引用 • 55 回帖 • 5 关注
  • 30Seconds

    📙 前端知识精选集,包含 HTML、CSS、JavaScript、React、Node、安全等方面,每天仅需 30 秒。

    • 精选常见面试题,帮助您准备下一次面试
    • 精选常见交互,帮助您拥有简洁酷炫的站点
    • 精选有用的 React 片段,帮助你获取最佳实践
    • 精选常见代码集,帮助您提高打码效率
    • 整理前端界的最新资讯,邀您一同探索新世界
    488 引用 • 384 回帖
  • sts
    2 引用 • 2 回帖 • 196 关注
  • SEO

    发布对别人有帮助的原创内容是最好的 SEO 方式。

    35 引用 • 200 回帖 • 27 关注
  • 微软

    微软是一家美国跨国科技公司,也是世界 PC 软件开发的先导,由比尔·盖茨与保罗·艾伦创办于 1975 年,公司总部设立在华盛顿州的雷德蒙德(Redmond,邻近西雅图)。以研发、制造、授权和提供广泛的电脑软件服务业务为主。

    8 引用 • 44 回帖
  • 脑图

    脑图又叫思维导图,是表达发散性思维的有效图形思维工具 ,它简单却又很有效,是一种实用性的思维工具。

    30 引用 • 96 回帖 • 1 关注
  • 黑曜石

    黑曜石是一款强大的知识库工具,支持本地 Markdown 文件编辑,支持双向链接和关系图。

    A second brain, for you, forever.

    16 引用 • 130 回帖
  • LaTeX

    LaTeX(音译“拉泰赫”)是一种基于 ΤΕΧ 的排版系统,由美国计算机学家莱斯利·兰伯特(Leslie Lamport)在 20 世纪 80 年代初期开发,利用这种格式,即使使用者没有排版和程序设计的知识也可以充分发挥由 TeX 所提供的强大功能,能在几天,甚至几小时内生成很多具有书籍质量的印刷品。对于生成复杂表格和数学公式,这一点表现得尤为突出。因此它非常适用于生成高印刷质量的科技和数学类文档。

    12 引用 • 54 回帖 • 49 关注
  • 开源

    Open Source, Open Mind, Open Sight, Open Future!

    407 引用 • 3578 回帖 • 1 关注
  • Jenkins

    Jenkins 是一套开源的持续集成工具。它提供了非常丰富的插件,让构建、部署、自动化集成项目变得简单易用。

    53 引用 • 37 回帖 • 3 关注
  • 新人

    让我们欢迎这对新人。哦,不好意思说错了,让我们欢迎这位新人!
    新手上路,请谨慎驾驶!

    52 引用 • 228 回帖
  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 158 关注
  • CodeMirror
    1 引用 • 2 回帖 • 129 关注
  • Git

    Git 是 Linux Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

    209 引用 • 358 回帖
  • PHP

    PHP(Hypertext Preprocessor)是一种开源脚本语言。语法吸收了 C 语言、 Java 和 Perl 的特点,主要适用于 Web 开发领域,据说是世界上最好的编程语言。

    179 引用 • 407 回帖 • 491 关注
  • Chrome

    Chrome 又称 Google 浏览器,是一个由谷歌公司开发的网页浏览器。该浏览器是基于其他开源软件所编写,包括 WebKit,目标是提升稳定性、速度和安全性,并创造出简单且有效率的使用者界面。

    62 引用 • 289 回帖
  • 七牛云

    七牛云是国内领先的企业级公有云服务商,致力于打造以数据为核心的场景化 PaaS 服务。围绕富媒体场景,七牛先后推出了对象存储,融合 CDN 加速,数据通用处理,内容反垃圾服务,以及直播云服务等。

    27 引用 • 225 回帖 • 162 关注