草庐IT

显示框架之VirtualDisplay 的数据源

Android曼巴 2023-03-28 原文

Android 支持多个屏幕,主屏(主显的物理屏),虚拟屏(非物理屏),外部显示屏(折叠屏),其中主屏和外部显示屏是实实在在的硬件物理屏,这两者在SurfaceFlinger侧的显示流程相差不大,而VirtualDisplay虽然也是走的SurfaceFlinger流程,但数据源的方式有较大的不同,本文就分析下VirtualDisplay的数据源。
对VirtualDisplay框架层的分析可以看下这篇文章:https://www.jianshu.com/p/c4ea60bc73d2
这里主要探索一下VirtualDisplay的数据源。

CreateDisplay

首先框架层会通过DMS来创建虚拟屏,通过jni调到SurfaceCompoerClient:: createDisplay,再通过binder调到SurfaceFlinger,看下SurfaceFlinger侧:

文件:frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

sp<IBinder> SurfaceFlinger::createDisplay(const String8& displayName, bool secure) {
 
    class DisplayToken : public BBinder {
        sp<SurfaceFlinger> flinger;
        virtual ~DisplayToken() {
             // no more references, this display must be terminated
             Mutex::Autolock _l(flinger->mStateLock);
             flinger->mCurrentState.displays.removeItem(this);
             flinger->setTransactionFlags(eDisplayTransactionNeeded);
         }
     public:
        explicit DisplayToken(const sp<SurfaceFlinger>& flinger)
            : flinger(flinger) {
        }
    };
    // new了一个token ,这个是可以跨进程传递的对象
    sp<BBinder> token = new DisplayToken(this);

    Mutex::Autolock _l(mStateLock);
    // Display ID is assigned when virtual display is allocated by HWC.
    DisplayDeviceState state;
    state.isSecure = secure;
    state.displayName = displayName;
    // 把display的状态存放在mCurrentState
    mCurrentState.displays.add(token, state);
    mInterceptor->saveDisplayCreation(state);
    return token;
}

CreateDisplay 的作用是创建了一个token返回给框架层,框架层通过这个token就能识别到这个display,然后存放到mCurrentState.displays 里面。

setDisplaySurface

WMS会通过这个接口给SurfaceFlinger传一个Surface,这个Surface是创建VirtualDisplay的进程用来显示内容的。要注意这个Surface与SurfaceFlinger不在同一个进程。


setDisplaySurface的callback.png
文件:frameworks/native/libs/gui/SurfaceComposerClient.cpp

status_t SurfaceComposerClient::Transaction::setDisplaySurface(const sp<IBinder>& token,
        const sp<IGraphicBufferProducer>& bufferProducer) {
    ...
    DisplayState& s(getDisplayState(token));
    // 设置DisplayState的surface
    s.surface = bufferProducer;
    s.what |= DisplayState::eSurfaceChanged;
    return NO_ERROR;
}

这个接口的主要作用就是设置surface给SurfaceFlinger,这个surface是个BufferQufferProducer对象,由vds所在的进程创建而成。

processDisplayAdded

有display发生变化时,transactionFlags 就会被置上eDisplayTransactionNeeded 这个flag,有新增加的display时,就会走processDisplayAdded这个逻辑,这个函数承载着主要的逻辑。

文件:frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

void SurfaceFlinger::processDisplayAdded(const wp<IBinder>& displayToken,
                                         const DisplayDeviceState& state) {
    ...
    // 物理屏的逻辑,虚拟屏不走
    if (state.physical) {
        const auto& activeConfig =
                getCompositionEngine().getHwComposer().getActiveConfig(state.physical->id);
        width = activeConfig->getWidth();
        height = activeConfig->getHeight();
        pixelFormat = static_cast<ui::PixelFormat>(PIXEL_FORMAT_RGBA_8888);
    } else if (state.surface != nullptr) {
        // 虚拟屏逻辑走这里,查询传进来的Surface的宽,高,格式
        int status = state.surface->query(NATIVE_WINDOW_WIDTH, &width);
        ALOGE_IF(status != NO_ERROR, "Unable to query width (%d)", status);
        status = state.surface->query(NATIVE_WINDOW_HEIGHT, &height);
        ALOGE_IF(status != NO_ERROR, "Unable to query height (%d)", status);
        int intPixelFormat;
        status = state.surface->query(NATIVE_WINDOW_FORMAT, &intPixelFormat);
        ALOGE_IF(status != NO_ERROR, "Unable to query format (%d)", status);
        pixelFormat = static_cast<ui::PixelFormat>(intPixelFormat);
        ...
    } else {
        // Virtual displays without a surface are dormant:
        // they have external state (layer stack, projection,
        // etc.) but no internal state (i.e. a DisplayDevice). 
        return;
    }

  compositionengine::DisplayCreationArgsBuilder builder;
    if (const auto& physical = state.physical) {
        // 如果是主屏则设置display id
        builder.setPhysical({physical->id, physical->type});
    }
    // 设置display的属性参数
    builder.setPixels(ui::Size(width, height));
    builder.setPixelFormat(pixelFormat);
    builder.setIsSecure(state.isSecure);
    builder.setLayerStackId(state.layerStack);
    builder.setPowerAdvisor(&mPowerAdvisor);
    // 设置是否支持使用HWC合成 VDS
    builder.setUseHwcVirtualDisplays((mUseHwcVirtualDisplays && canAllocateHwcForVDS) ||
                                     getHwComposer().isUsingVrComposer());
    builder.setName(state.displayName);
    
     // 创建compositionDisplay,这个函数的作用是创建对应的Output和Display
    const auto compositionDisplay = getCompositionEngine().createDisplay(builder.build());

    sp<compositionengine::DisplaySurface> displaySurface;
    sp<IGraphicBufferProducer> producer;
    sp<IGraphicBufferProducer> bqProducer;
    sp<IGraphicBufferConsumer> bqConsumer;

    // 创建一个BufferQueue,拿到对应的BufferQueueProducer和BufferQueueConsumer
    getFactory().createBufferQueue(&bqProducer, &bqConsumer, /*consumerIsSurfaceFlinger =*/false);

    std::optional<DisplayId> displayId = compositionDisplay->getId();

    if (state.isVirtual()) {
        // 创建VirtualDisplaySurface
        sp<VirtualDisplaySurface> vds =
                new VirtualDisplaySurface(getHwComposer(), displayId, state.surface,
                                          bqProducer, bqConsumer, state.displayName,
                                          state.isSecure);

         // 将vds设置给displaySurface 和 producer 
        displaySurface = vds;
        producer = vds;
    } else {
       // 主屏会创建FrameBufferSurface
        ...
    }

   // 创建nativeWindowSurface和displaydevice
  const auto display = setupNewDisplayDeviceInternal(displayToken, compositionDisplay, state,
                                                       displaySurface, producer);
  mDisplays.emplace(displayToken, display);
  ...
}

(1) getCompositionEngine().createDisplay(builder.build()) 会创建相应的Output和 Display对象,对应的类序图如下:


display的类序图.png

(2)new VirtualDisplaySurface,创建VDS,将surface作为 mSource[SOURCE_SINK]

文件:frameworks/native/services/surfaceflinger/DisplayHardware/VirtualDisplaySurface.cpp

VirtualDisplaySurface::VirtualDisplaySurface(HWComposer& hwc,
                                             const std::optional<DisplayId>& displayId,
                                             const sp<IGraphicBufferProducer>& sink,
                                             const sp<IGraphicBufferProducer>& bqProducer,
                                             const sp<IGraphicBufferConsumer>& bqConsumer,
                                             const std::string& name, bool secure)
      : ConsumerBase(bqConsumer),
       ... {
    // 将surface作为mSource[SOURCE_SINK], BufferQueueProducer作为mSource[SOURCE_SCRATCH]
    mSource[SOURCE_SINK] = sink;
    mSource[SOURCE_SCRATCH] = bqProducer;

    resetPerFrameState();
                                                
    int sinkWidth, sinkHeight;

    // 查询surface的宽高
    sink->query(NATIVE_WINDOW_WIDTH, &sinkWidth);
    sink->query(NATIVE_WINDOW_HEIGHT, &sinkHeight);
    mSinkBufferWidth = sinkWidth;
    mSinkBufferHeight = sinkHeight;
                                                       
    //  查询和设置usage,format
    int sinkUsage;
    sink->query(NATIVE_WINDOW_CONSUMER_USAGE_BITS, &sinkUsage);
    mSinkUsage |= (GRALLOC_USAGE_HW_COMPOSER | sinkUsage);
    setOutputUsage(mSinkUsage);
    if (sinkUsage & (GRALLOC_USAGE_SW_READ_MASK | GRALLOC_USAGE_SW_WRITE_MASK)) {
        int sinkFormat;
        sink->query(NATIVE_WINDOW_FORMAT, &sinkFormat);
        mDefaultOutputFormat = sinkFormat;
    } else {
        mDefaultOutputFormat = HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED;
    }
    mOutputFormat = mDefaultOutputFormat;

    // 设置BufferQueueConsumer的name,usage,BufferSize
    ConsumerBase::mName = String8::format("VDS: %s", mDisplayName.c_str());
    mConsumer->setConsumerName(ConsumerBase::mName);
    mConsumer->setConsumerUsageBits(GRALLOC_USAGE_HW_COMPOSER);
    mConsumer->setDefaultBufferSize(sinkWidth, sinkHeight);
    sink->setAsyncMode(true);
    IGraphicBufferProducer::QueueBufferOutput output;
    mSource[SOURCE_SCRATCH]->connect(nullptr, NATIVE_WINDOW_API_EGL, false, &output);
}

这一步最重要的就是将surface设置成了mSource[SOURCE_SINK]。
(3) setupNewDisplayDeviceInternal 这个函数创建了nativeWindowSurface和displaydevice

文件:frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

sp<DisplayDevice> SurfaceFlinger::setupNewDisplayDeviceInternal(
        const wp<IBinder>& displayToken,
        std::shared_ptr<compositionengine::Display> compositionDisplay,
        const DisplayDeviceState& state,
        const sp<compositionengine::DisplaySurface>& displaySurface,
        const sp<IGraphicBufferProducer>& producer) {

     ...
    // 将producer作为参数创建一个NativeWindowSurface,这个producer就是vds
    auto nativeWindowSurface = getFactory().createNativeWindowSurface(producer);
    auto nativeWindow = nativeWindowSurface->getNativeWindow();
    creationArgs.nativeWindow = nativeWindow;

    // Make sure that composition can never be stalled by a virtual display
    // consumer that isn't processing buffers fast enough. We have to do this
    // here, in case the display is composed entirely by HWC.
    if (state.isVirtual()) {
        nativeWindow->setSwapInterval(nativeWindow.get(), 0);
    }
    ...

这里重要的是将vds作为producer创建了Surface


vds类序图.png
文件:frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

sp<DisplayDevice> SurfaceFlinger::setupNewDisplayDeviceInternal(
        const wp<IBinder>& displayToken,
        std::shared_ptr<compositionengine::Display> compositionDisplay,
        const DisplayDeviceState& state,
        const sp<compositionengine::DisplaySurface>& displaySurface,
        const sp<IGraphicBufferProducer>& producer) {

   ...
    // 虚拟屏一直设为power on
    creationArgs.initialPowerMode = state.isVirtual() ? hal::PowerMode::ON : hal::PowerMode::OFF;

    // 创建DisplayDevice
    sp<DisplayDevice> display = getFactory().createDisplayDevice(creationArgs);

   ...
    // 设置vds displaydevice的参数
    display->setLayerStack(state.layerStack);
    display->setProjection(state.orientation, state.viewport, state.frame);
    display->setDisplayName(state.displayName);
}

文件:frameworks/native/services/surfaceflinger/DisplayDevice.cpp

DisplayDevice::DisplayDevice(DisplayDeviceCreationArgs& args)
      : mFlinger(args.flinger),
        mDisplayToken(args.displayToken),
        mSequenceId(args.sequenceId),
        mConnectionType(args.connectionType),
        mCompositionDisplay{args.compositionDisplay},
        mPhysicalOrientation(args.physicalOrientation),
        mIsPrimary(args.isPrimary) {

    mCompositionDisplay->editState().isSecure = args.isSecure;
    // 创建RenderSurface,将vds 和 windowSurface 作为参数传进来
    mCompositionDisplay->createRenderSurface(
            compositionengine::RenderSurfaceCreationArgs{ANativeWindow_getWidth(
                                                                 args.nativeWindow.get()),
                                                         ANativeWindow_getHeight(
                                                                 args.nativeWindow.get()),
                                                         args.nativeWindow, args.displaySurface});

    ...
}

文件:frameworks/native/services/surfaceflinger/CompositionEngine/src/RenderSurface.cpp

RenderSurface::RenderSurface(const CompositionEngine& compositionEngine, Display& display,
                             const RenderSurfaceCreationArgs& args)
      : mCompositionEngine(compositionEngine),
        mDisplay(display),
        //  mNativeWindow 为 surface对象   mDisplaySurface 为vds对象
        mNativeWindow(args.nativeWindow),
        mDisplaySurface(args.displaySurface),
        mSize(args.displayWidth, args.displayHeight) {
    LOG_ALWAYS_FATAL_IF(!mNativeWindow);
}

这一步最重要的就是创建了nativeWindowSurface和displaydevice对象,到这里初始化的流程就走完了。

数据源

vds创建后,跟着SurfaceFlinger主线程进行刷新,SurfaceFlinger refresh的几个接口在“显示框架之SurfaceFlinger Refresh流程”里面有分析,这个流程对于VDS没差,但有些函数调用有差别,来看下:
Refresh主要执行的几个函数:

文件:frameworks/native/services/surfaceflinger/CompositionEngine/src/Output.cpp

void Output::present(const compositionengine::CompositionRefreshArgs& refreshArgs) {
    ATRACE_CALL();
    ALOGV(__FUNCTION__);

    updateColorProfile(refreshArgs);
    updateAndWriteCompositionState(refreshArgs);
    setColorTransform(refreshArgs);
    beginFrame();
    prepareFrame();
    devOptRepaintFlash(refreshArgs);
    finishFrame(refreshArgs);
    postFramebuffer();
}

beginFrame

文件:frameworks/native/services/surfaceflinger/CompositionEngine/src/Output.cpp

void Output::beginFrame() {
    ...
    mRenderSurface->beginFrame(mustRecompose);

    if (mustRecompose) {
        outputState.lastCompositionHadVisibleLayers = !empty;
    }
}

文件:frameworks/native/services/surfaceflinger/CompositionEngine/src/RenderSurface.cpp

status_t RenderSurface::beginFrame(bool mustRecompose) {
    return mDisplaySurface->beginFrame(mustRecompose);
}

文件:frameworks/native/services/surfaceflinger/DisplayHardware/VirtualDisplaySurface.cpp

status_t VirtualDisplaySurface::beginFrame(bool mustRecompose) {
    // 因为hwc暂不支持vds功能,故这里displayid为null,直接return
    if (!mDisplayId) {
        return NO_ERROR;
    }

    mMustRecompose = mustRecompose;
    //For WFD use cases we must always set the recompose flag in order
    //to support pause/resume functionality
    if (mOutputUsage & GRALLOC_USAGE_HW_VIDEO_ENCODER) {
        mMustRecompose = true;
    }
    VDS_LOGW_IF(mDbgState != DBG_STATE_IDLE,
            "Unexpected beginFrame() in %s state", dbgStateStr());
    mDbgState = DBG_STATE_BEGUN;

    return refreshOutputBuffer();
}

注意这里因为hwc不支持vds,故displayid 为null,直接return,没做什么事情。

prepareFrame

文件:frameworks/native/services/surfaceflinger/CompositionEngine/src/Output.cpp

void Output::prepareFrame() {
    ATRACE_CALL();
    ALOGV(__FUNCTION__);

    const auto& outputState = getState();
    if (!outputState.isEnabled) {
        return;
    }
     // 没有hwcid,故直接走GPU合成
    chooseCompositionStrategy();

    mRenderSurface->prepareFrame(outputState.usesClientComposition,
                                 outputState.usesDeviceComposition);
}

文件:frameworks/native/services/surfaceflinger/CompositionEngine/src/RenderSurface.cpp

void RenderSurface::prepareFrame(bool usesClientComposition, bool usesDeviceComposition) {
    DisplaySurface::CompositionType compositionType;
   ...
    } else if (usesClientComposition) {
        // 这里直接走GPU合成
        compositionType = DisplaySurface::COMPOSITION_GPU;
    ...
    if (status_t result = mDisplaySurface->prepareFrame(compositionType); result != NO_ERROR) {
        ALOGE("updateCompositionType failed for %s: %d (%s)", mDisplay.getName().c_str(), result,
              strerror(-result));
    }
}

文件:frameworks/native/services/surfaceflinger/DisplayHardware/VirtualDisplaySurface.cpp

status_t VirtualDisplaySurface::prepareFrame(CompositionType compositionType) {
    // 没有Displayid 直接return
    if (!mDisplayId) {
        return NO_ERROR;
    }

   ...
}

这个函数的作用就是判断了vds的合成类型,因为hwc不支持的原因,所以目前走GPU合成。

finishFrame

文件:frameworks/native/services/surfaceflinger/CompositionEngine/src/Output.cpp

void Output::finishFrame(const compositionengine::CompositionRefreshArgs& refreshArgs) {
    ...
    // dequeueBuffer
    auto optReadyFence = composeSurfaces(Region::INVALID_REGION, refreshArgs);
    if (!optReadyFence) {
        return;
    }

    // swap buffers (presentation)
    mRenderSurface->queueBuffer(std::move(*optReadyFence));
}

std::optional<base::unique_fd> Output::composeSurfaces(
        const Region& debugRegion, const compositionengine::CompositionRefreshArgs& refreshArgs) {
    ...
    base::unique_fd fd;
    sp<GraphicBuffer> buf;

    // If we aren't doing client composition on this output, but do have a
    // flipClientTarget request for this frame on this output, we still need to
    // dequeue a buffer.
    if (hasClientComposition || outputState.flipClientTarget) {
        buf = mRenderSurface->dequeueBuffer(&fd);
        if (buf == nullptr) {
            ALOGW("Dequeuing buffer for display [%s] failed, bailing out of "
                  "client composition for this frame",
                  mName.c_str());
            return {};
        }
    }
   // GPU合成的逻辑
   ...
}

文件:frameworks/native/services/surfaceflinger/CompositionEngine/src/RenderSurface.cpp

sp<GraphicBuffer> RenderSurface::dequeueBuffer(base::unique_fd* bufferFence) {
    ATRACE_CALL();
    int fd = -1;
    ANativeWindowBuffer* buffer = nullptr;

    // 主要的区别在这里,mNativeWindow 为Surface对象,调到Surface.cpp里面的dequeueBuffer,然后再调到VirtualDisplay的deququBuffer
    status_t result = mNativeWindow->dequeueBuffer(mNativeWindow.get(), &buffer, &fd);

    if (result != NO_ERROR) {
        ALOGE("ANativeWindow::dequeueBuffer failed for display [%s] with error: %d",
              mDisplay.getName().c_str(), result);
        // Return fast here as we can't do much more - any rendering we do
        // now will just be wrong.
        return mGraphicBuffer;
    }

    ALOGW_IF(mGraphicBuffer != nullptr, "Clobbering a non-null pointer to a buffer [%p].",
             mGraphicBuffer->getNativeBuffer()->handle);
    mGraphicBuffer = GraphicBuffer::from(buffer);

    *bufferFence = base::unique_fd(fd);

    return mGraphicBuffer;
}

文件:frameworks/native/libs/gui/Surface.cpp 

int Surface::dequeueBuffer(android_native_buffer_t** buffer, int* fenceFd) {
...
  // 这里mGraphicBufferProducer 对象为VirtualDisplaySurface
 status_t result = mGraphicBufferProducer->dequeueBuffer(&buf, &fence, reqWidth, reqHeight,
                                                          reqFormat, reqUsage, &mBufferAge,
                                                          enableFrameTimestamps ? &frameTimestamps
                                                                                   : nullptr);
...
}

文件:frameworks/native/services/surfaceflinger/DisplayHardware/VirtualDisplaySurface.cpp

status_t VirtualDisplaySurface::dequeueBuffer(int* pslot, sp<Fence>* fence, uint32_t w, uint32_t h,
                                              PixelFormat format, uint64_t usage,
                                              uint64_t* outBufferAge,
                                              FrameEventHistoryDelta* outTimestamps) {
    if (!mDisplayId) {
        // 这里执行的是mSource[SOURCE_SINK] 的dequeueBuffer,mSource[SOURCE_SINK] 实质上就是应用传进来的surface
        return mSource[SOURCE_SINK]->dequeueBuffer(pslot, fence, w, h, format, usage, outBufferAge,
                                                   outTimestamps);
    }

这里最重要的就是理解dequeueBuffer的执行对象是谁,RenderSurface::dequeueBuffer -> NativeWindow:: dequeueBuffer -> Surface::dequeueBuffer ->VirtualDisplaySurface:: dequeueBuffer-> sf对端进程的Surface::dequeueBuffer
同理,queueBuffer的执行对象跟dequeueBuffer一样,RenderSurface::queueBuffer -> NativeWindow:: queueBuffer -> Surface::queueBuffer ->VirtualDisplaySurface:: queueBuffer-> sf对端进程的Surface::queueBuffer
可以看出来dequeueBuffer和queueBuffer都是在sf对端进程实现,从systrace也可以看到,这里SurfaceFlinger作为Client端,media.codec为Server端。


SurfaceFlinger Client端.png

media.codec server端.png

在 ”显示框架之SurfaceFlinger GPU合成 “ 分析到dequeueBuffer出来的Buffer作为输出的Buffer,输入为当前layer的Buffer,可以理解为GPU将输入的n块Buffer合成输出到1块Buffer,具体流程可以看“显示框架之SurfaceFlinger GPU合成” 的分析,可以看到其实数据源就是GPU合成的这块Buffer, 交给media.codec去消费,这块Buffer的acquire进程也是media.codec。


media.codec acquireBuffer.png

之后Buffer就给到media去处理了


mediaserver处理buffer.png

总结:对于VDS,SurfaceFlinger是作为Client端,GPU合成的结果是数据源,所以虚拟屏显示的内容和主屏是一样的。

有关显示框架之VirtualDisplay 的数据源的更多相关文章

  1. ruby-on-rails - Rails 编辑表单不显示嵌套项 - 2

    我得到了一个包含嵌套链接的表单。编辑时链接字段为空的问题。这是我的表格:Editingkategori{:action=>'update',:id=>@konkurrancer.id})do|f|%>'Trackingurl',:style=>'width:500;'%>'Editkonkurrence'%>|我的konkurrencer模型:has_one:link我的链接模型:classLink我的konkurrancer编辑操作:defedit@konkurrancer=Konkurrancer.find(params[:id])@konkurrancer.link_attrib

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby-on-rails - 使用 Sublime Text 3 突出显示 HTML 背景语法中的 ERB? - 2

    所以我在关注Railscast,我注意到在html.erb文件中,ruby代码有一个微弱的背景高亮效果,以区别于其他代码HTML文档。我知道Ryan使用TextMate。我正在使用SublimeText3。我怎样才能达到同样的效果?谢谢! 最佳答案 为SublimeText安装ERB包。假设您安装了SublimeText包管理器*,只需点击cmd+shift+P即可获得命令菜单,然后键入installpackage并选择PackageControl:InstallPackage获取包管理器菜单。在该菜单中,键入ERB并在看到包时选择

  4. ruby-on-rails - link_to 不显示任何 rails - 2

    我试图在索引页中创建一个超链接,但它没有显示,也没有给出任何错误。这是我的index.html.erb代码。ListingarticlesTitleTextssss我检查了我的路线,我认为它们也没有问题。PrefixVerbURIPatternController#Actionwelcome_indexGET/welcome/index(.:format)welcome#indexarticlesGET/articles(.:format)articles#indexPOST/articles(.:format)articles#createnew_articleGET/article

  5. ruby-on-rails - 如何在 Rails View 上显示错误消息? - 2

    我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c

  6. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  7. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  8. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  9. ruby-on-rails - 复数 for fields_for has_many 关联未显示在 View 中 - 2

    目前,Itembelongs_toCompany和has_manyItemVariants。我正在尝试使用嵌套的fields_for通过Item表单添加ItemVariant字段,但是使用:item_variants不显示该表单。只有当我使用单数时才会显示。我检查了我的关联,它们似乎是正确的,这可能与嵌套在公司下的项目有关,还是我遗漏了其他东西?提前致谢。注意:下面的代码片段中省略了不相关的代码。编辑:不知道这是否相关,但我正在使用CanCan进行身份验证。routes.rbresources:companiesdoresources:itemsenditem.rbclassItemi

  10. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

随机推荐